Está en la página 1de 2281

Contents

Introducción
Novedades
Novedades
Novedades
Introducción
Creación de una aplicación web
Creación de una API Web
Tutoriales
Creación de una aplicación web de páginas de Razor
Introducción a las páginas de Razor
Agregar un modelo
Páginas de Razor con scaffolding
SQL Server LocalDB
Actualización de las páginas
Agregar búsqueda
Agregar un campo nuevo
Agregar validación
Carga de archivos
Creación de una aplicación web MVC
Introducción
Agregar un controlador
Agregar una vista
Agregar un modelo
Trabajar con SQL Server LocalDB
Vistas y métodos de controlador
Agregar búsqueda
Agregar un campo nuevo
Agregar validación
Examinar los métodos Details y Delete
Compilación de API web
Crear una Web API en Visual Studio Code
Crear una Web API en Visual Studio para Mac
Crear una Web API en Visual Studio para Windows
Creación de servicios back-end para aplicaciones móviles nativas
Páginas de ayuda mediante Swagger
Get started with NSwag
Get started with Swashbuckle
Acceso a datos con EF Core
Acceso a datos con páginas de Razor y EF Core
Introducción
Operaciones de creación, lectura, actualización y eliminación
Ordenación, filtrado, paginación y agrupación
Migraciones
Creación de un modelo de datos complejo
Lectura de datos relacionados
Actualización de datos relacionados
Control de conflictos de simultaneidad
Acceso a datos: MVC con EF Core
Introducción
Operaciones de creación, lectura, actualización y eliminación
Ordenación, filtrado, paginación y agrupación
Migraciones
Creación de un modelo de datos complejo
Lectura de datos relacionados
Actualización de datos relacionados
Control de conflictos de simultaneidad
Herencia
Temas avanzados
Tutoriales multiplataforma
Aplicación web de páginas de Razor en macOS
Introducción a las páginas de Razor
Agregar un modelo
Páginas de Razor con scaffolding
Trabajar con SQLite
Actualización de las páginas
Agregar búsqueda
Aplicación web de páginas de Razor con VS Code
Introducción a las páginas de Razor
Agregar un modelo
Páginas de Razor con scaffolding
Trabajar con SQLite
Actualización de las páginas
Agregar búsqueda
Aplicación web MVC con Visual Studio para Mac
Introducción
Agregar un controlador
Agregar una vista
Agregar un modelo
Trabajar con SQLite
Vistas y métodos de controlador
Agregar búsqueda
Agregar un campo nuevo
Agregar validación
Examinar los métodos Details y Delete
Aplicación web MWC con Visual Studio Code en macOS o Linux
Introducción
Agregar un controlador
Agregar una vista
Agregar un modelo
Trabajar con SQLite
Vistas y métodos de controlador
Agregar búsqueda
Agregar un campo nuevo
Agregar validación
Examinar los métodos Details y Delete
API web con Visual Studio para Mac
API web con Visual Studio Code
Desarrollo de aplicaciones mediante un monitor de archivos
Creación de servicios back-end para aplicaciones móviles
Aspectos básicos
Inicio de aplicaciones
Inserción de dependencias (servicios)
Middleware
Middleware
Middleware basado en Factory
Middleware basado en Factory con un contenedor de terceros
Archivos estáticos
Enrutamiento
Middleware de reescritura de dirección URL
Uso de varios entornos
Configuración y opciones
Configuración
Opciones
Mejora de una aplicación a partir de un ensamblado externo
Registro
Registro con LoggerMessage
Control de errores
Proveedores de archivos
Host
Host web
Host genérico
Tareas en segundo plano con servicios hospedados
Estado de sesión y aplicación
Servidores
Kestrel
Módulo ASP.NET Core
HTTP.sys
Globalización y localización
Configuración de la localización de un objeto portable con Orchard Core
Inicio de solicitudes HTTP
Características de la solicitud
Elementos primitivos
Cambio de tokens
Apertura de la interfaz web para .NET (OWIN)
WebSockets
Metapaquete Microsoft.AspNetCore.App
Metapaquete Microsoft.AspNetCore.All
Elección entre .NET Core y .NET Framework
Elección entre ASP.NET y ASP.NET Core
Páginas de Razor
Métodos de filtrado para páginas de Razor
Creación de una biblioteca de clases de Razor
Convenciones de rutas y aplicaciones
SDK de Razor
MVC
Enlace de modelos
Validación de modelos
Vistas
Sintaxis de Razor
Visualización de compilación
Diseño
Aplicaciones auxiliares de etiquetas
Creación de aplicaciones auxiliares de etiquetas
Uso de aplicaciones auxiliares de etiquetas en formularios
Aplicaciones auxiliares de etiquetas integradas
Aplicación auxiliar de etiquetas de delimitador
Aplicación auxiliar de etiquetas de caché
Aplicación auxiliar de etiquetas de caché distribuida
Aplicación auxiliar de etiquetas de entorno
Aplicación auxiliar de etiquetas de formulario
Aplicación auxiliar de etiquetas de imagen
Aplicación auxiliar de etiquetas de entrada
Aplicación auxiliar de etiquetas de elementos de etiqueta
Aplicación auxiliar de etiquetas parciales
Aplicación auxiliar de etiquetas de selección
Aplicación auxiliar de etiquetas de área de texto
Aplicación auxiliar de etiquetas de mensaje de validación
Aplicación auxiliar de etiquetas de resumen de validación
Vistas parciales
Inserción de dependencias en vistas
Visualización de componentes
Controladores
Enrutamiento a acciones del controlador
Cargas de archivos
Inserción de dependencias en controladores
Controladores de pruebas
Avanzadas
Trabajar con el modelo de aplicación
Filtros
Áreas
Elementos de la aplicación
Enlace de modelos personalizado
API Web
Tipos de valor devuelto de acción del controlador
Avanzadas
Formateadores personalizados
Aplicación de formato a datos de respuesta
Probar, depurar y solucionar problemas
Pruebas unitarias
Pruebas de integración
Pruebas unitarias de páginas de Razor
Controladores de pruebas
Depuración remota
Depuración de instantáneas
Depuración en Visual Studio
Solucionar problemas
Acceso a datos con EF Core y Azure
Introducción a las páginas de Razor y EF Core con Visual Studio
Introducción a ASP.NET Core y EF Core con Visual Studio
ASP.NET Core con EF Core: nueva base de datos
ASP.NET Core con EF Core: base de datos existente
Introducción a ASP.NET Core y Entity Framework 6
Azure Storage
Agregar Azure Storage mediante el uso de Servicios conectados de Visual Studio
Introducción a Blob Storage y Servicios conectados de Visual Studio
Introducción a Queue Storage y Servicios conectados de Visual Studio
Introducción a Table Storage y Servicios conectados de Visual Studio
Desarrollo del lado cliente
Uso de Gulp
Uso de Grunt
Administración de paquetes de cliente con Bower
Creación de sitios con capacidad de respuesta con Bootstrap
Aplicación de estilo a aplicaciones con LESS, Sass y Font Awesome
Agrupar y minimizar
Uso de Vínculo con exploradores
Uso de JavaScriptServices para aplicaciones de página única
Uso de plantillas de proyectos de aplicaciones de página única
Plantilla de proyecto Angular
Plantilla de proyecto React
Plantilla de proyecto React con Redux
SignalR
Introducción
Introducción
Introducción
Concentradores
Cliente de JavaScript
Cliente .NET
HubContext
Usuarios y grupos
Protocolo de concentrador MessagePack
Publicar en Azure
Streaming
Plataformas compatibles
Móvil
Creación de servicios back-end para aplicaciones móviles nativas
Hospedaje e implementación
Hospedaje en Azure App Service
Publicación en Azure con Visual Studio
Publicación en Azure con herramientas de CLI
Implementación continua en Azure con Visual Studio y Git
Implementación continua en Azure con VSTS
Solución de problemas de ASP.NET Core en Azure App Service
Hospedaje en Windows con IIS
Solución de problemas de ASP.NET Core en IIS
Referencia de configuración del módulo ASP.NET Core
Compatibilidad de IIS de tiempo de desarrollo en Visual Studio para ASP.NET Core
Módulos de IIS con ASP.NET Core
Hospedaje en un servicio de Windows
Hospedaje en Linux con Nginx
Hospedaje en Linux con Apache
Hospedaje en Docker
Creación de imágenes de Docker
Visual Studio Tools para Docker
Publicación en una imagen de Docker
Configuración del proxy y del equilibrador de carga
Perfiles de publicación de Visual Studio
Estructura de directorios
Referencia de errores comunes de Azure App Service e IIS
Seguridad
Autenticación
Introducción a Identity
Identidad de scaffolding
Agregar datos de usuario personalizados a Identity
Opciones de autenticación de OSS de la comunidad
Configuración de Identity
Configuración de la autenticación de Windows
Configuración del tipo de clave principal para Identity
Proveedores de almacenamiento personalizados para la identidad
Habilitar la autenticación con Facebook, Google y otros proveedores externos
Autenticación con Facebook
Autenticación con Twitter
Autenticación con Google
Autenticación con Microsoft
Otros proveedores de autenticación
Autenticación con WS-Federation
Confirmación de cuentas y recuperación de contraseñas
Habilitar la generación de código QR en Identity
Autenticación en dos fases con SMS
Uso de la autenticación de cookies sin identidad
Azure Active Directory
Integración de Azure AD en una aplicación web de ASP.NET Core
Integración de Azure AD B2C en una aplicación web de ASP.NET Core dirigida a
los clientes
Integración de Azure AD B2C en una API web de ASP.NET Core
Llamada a una API web de ASP.NET Core desde una aplicación de WPF con Azure
AD
Llamada a una API web en una aplicación web de ASP.NET Core con Azure AD
Protección de aplicaciones de ASP.NET Core con IdentityServer4
Protección de aplicaciones de ASP.NET Core con la autenticación de Azure App
Service (autenticación sencilla)
Cuentas de usuario individuales
Autorización
Introducción
Creación de una aplicación con datos de usuario protegidos por autorización
Autorización de páginas de Razor
Autorización simple
Autorización basada en roles
Autorización basada en notificaciones
Autorización basada en directivas
Inserción de dependencias en controladores de requisitos
Autorización basada en recursos
Autorización basada en visualizaciones
Limitación de la identidad por esquema
Protección de datos
Introducción a la protección de datos
Introducción a las API de protección de datos
API de consumidor
Información general sobre las API de consumidor
Cadenas de propósito
Jerarquía de propósito y configuración multiempresa
Aplicar un algoritmo hash a las contraseñas
Limitación de la duración de cargas protegidas
Desprotección de cargas cuyas claves se han revocado
Configuración
Configuración de la protección de datos
Configuración predeterminada
Directiva de todo el equipo
Escenarios no compatibles con DI
API de extensibilidad
Extensibilidad de criptografía de núcleo
Extensibilidad de administración de claves
Otras API
Implementación
Detalles de cifrado autenticado
Derivación de subclave y cifrado autenticado
Encabezados de contexto
Administración de claves
Proveedores de almacenamiento de claves
Cifrado de claves en reposo
Inmutabilidad de claves y configuración
Formato de almacenamiento de claves
Proveedores de protección de datos efímeros
Compatibilidad
Reemplazar <machineKey> en ASP.NET
Aplicación de HTTPS
Compatibilidad con el Reglamento general de protección de datos (GDPR) de la UE
Almacenamiento seguro de secretos de aplicación en el desarrollo
Proveedor de configuración de Azure Key Vault
Prevención de ataques de falsificación de solicitudes
Prevención de ataques de redireccionamiento abierto
Prevención de scripting entre sitios
Habilitar solicitudes entre orígenes (CORS)
Compartir cookies entre aplicaciones
Rendimiento
Almacenamiento en caché de respuestas
Almacenamiento en caché en memoria
Trabajar con una memoria caché distribuida
Almacenamiento en caché de respuestas
Middleware de almacenamiento en caché de respuestas
Middleware de compresión de respuestas
Migración
ASP.NET Core 2.0 a 2.1
De ASP.NET a ASP.NET Core
MVC
API Web
Configuración
Autenticación e identidad
ClaimsPrincipal.Current
De pertenencia a identidad
Módulos HTTP a middleware
De ASP.NET Core 1.x a 2.0
Autenticación e identidad
Referencia de API
Colaboracion
Introducción a ASP.NET Core
21/06/2018 • 6 minutes to read • Edit Online

Por Daniel Roth, Rick Anderson y Shaun Luttin


ASP.NET Core es un marco multiplataforma de código abierto y de alto rendimiento que tiene como finalidad
compilar modernas aplicaciones conectadas a Internet y basadas en la nube. Con ASP.NET Core puede hacer lo
siguiente:
Compilar servicios y aplicaciones web, aplicaciones de IoT y back-ends móviles.
Usar sus herramientas de desarrollo favoritas en Windows, macOS y Linux.
Efectuar implementaciones locales y en la nube.
Ejecutarlo en .NET Core o en .NET Framework.

¿Por qué debería usar ASP.NET Core?


Millones de desarrolladores han usado ASP.NET 4.x (y siguen usándolo) para crear aplicaciones web. ASP.NET
Core es un nuevo diseño de ASP.NET 4.x que cuenta con cambios en la arquitectura que dan como resultado un
marco más sencillo y modular.
ASP.NET Core ofrece las siguientes ventajas:
Un caso unificado para crear API web y una interfaz de usuario web.
Integración de marcos del lado cliente modernos y flujos de trabajo de desarrollo.
Un sistema de configuración basado en el entorno y preparado para la nube.
Inserción de dependencias integrada.
Una canalización de solicitudes HTTP ligera, modular y de alto rendimiento.
Capacidad de hospedarse en IIS, Nginx, Apache, Docker o de autohospedarse en su propio proceso.
Control de versiones de aplicaciones en paralelo con .NET Core como destino.
Herramientas que simplifican el desarrollo web moderno.
Capacidad para compilarse y ejecutarse en Windows, macOS y Linux.
De código abierto y centrado en la comunidad.
ASP.NET Core se distribuye en su totalidad como paquetes NuGet. El uso de paquetes NuGet permite optimizar la
aplicación para incluir únicamente las dependencias necesarias. De hecho, las aplicaciones ASP.NET Core 2.x que
tienen .NET Core como destino solo requieren un paquete NuGet único. Entre las ventajas de una menor
superficie de aplicación se incluyen una mayor seguridad, un mantenimiento reducido y un rendimiento mejorado.

Creación de API web e interfaces de usuario web mediante ASP.NET


Core MVC
ASP.NET Core MVC proporciona características para crear API web y aplicaciones web:
El patrón Modelo-Vista-Controlador (MVC ) permite que se puedan hacer pruebas en las API web y en las
aplicaciones web.
Páginas de Razor (novedad de ASP.NET Core 2.0) es un modelo de programación basado en páginas que
facilita la compilación de interfaces de usuario web y hace que sea más productiva.
El marcado de Razor proporciona una sintaxis productiva para las páginas de Razor y las vistas de MVC.
Las aplicaciones auxiliares de etiquetas permiten que el código de servidor participe en la creación y la
representación de elementos HTML en archivos de Razor.
La compatibilidad integrada para varios formatos de datos y la negociación de contenidos permite que las API
web lleguen a una amplia gama de clientes, como los exploradores y los dispositivos móviles.
El enlace de modelo asigna automáticamente datos de solicitudes HTTP a parámetros de método de acción.
La validación de modelos efectúa una validación del lado cliente y del lado servidor de forma automática.

Desarrollo del lado cliente


ASP.NET Core se integra perfectamente con bibliotecas y marcos de trabajo populares del lado cliente, que
incluyen Angular, React y Bootstrap. Para obtener más información, vea Desarrollo del lado cliente.

ASP.NET Core con .NET Framework como destino


ASP.NET Core puede tener como destino .NET Core o .NET Framework. Las aplicaciones de ASP.NET Core que
tienen como destino .NET Framework no son multiplataforma, sino que solo se ejecutan en Windows. No está
previsto eliminar la compatibilidad con .NET Framework como destino en ASP.NET Core. Por lo general, ASP.NET
Core está formado por bibliotecas de .NET Standard. Las aplicaciones escritas con .NET Standard 2.0 se ejecutan
en cualquier lugar en el que se admita .NET Standard 2.0.
El uso de .NET Core como destino cuenta con varias ventajas que van en aumento con cada versión. Entre las
ventajas del uso de .NET Core en vez de .NET Framework se incluyen las siguientes:
Multiplataforma. Ejecución en macOS, Linux y Windows.
Rendimiento mejorado
Control de versiones en paralelo.
Nuevas API.
Abrir origen
Estamos trabajando intensamente para cerrar la brecha de API entre .NET Framework y .NET Core. El paquete de
compatibilidad de Windows ha permitido que miles de API solo de Windows estén disponibles en .NET Core.
Estas API no estaban disponibles en .NET Core 1.x.

Pasos siguientes
Para obtener más información, vea los siguientes recursos:
Introducción a las páginas de Razor
Tutoriales de ASP.NET Core
Conceptos básicos de ASP.NET Core
La reunión semanal de la comunidad de ASP.NET trata el progreso y los planes del equipo. Incluye nuevos
blogs y nuevo software de terceros.
2 minutes to read
Crear una API web con ASP.NET Core y Visual Studio
para Windows
21/06/2018 • 25 minutes to read • Edit Online

Por Rick Anderson y Mike Wasson


En este tutorial se compila una API web para administrar una lista de tareas pendientes. No se crea ninguna
interfaz de usuario (UI).
Hay tres versiones de este tutorial:
Windows: API web con Visual Studio para Windows (este tutorial)
macOS: API web con Visual Studio para Mac
macOS, Linux y Windows: API web con Visual Studio Code

Información general
En este tutorial se crea la siguiente API:

API DESCRIPTION CUERPO DE LA SOLICITUD CUERPO DE LA RESPUESTA

GET /api/todo Obtener todas las tareas Ninguna Matriz de tareas pendientes
pendientes

GET /api/todo/{id} Obtener un elemento por Ninguna Tarea pendiente


identificador

POST /api/todo Agregar un nuevo elemento Tarea pendiente Tarea pendiente

PUT /api/todo/{id} Actualizar un elemento Tarea pendiente Ninguna


existente

DELETE /api/todo/{id} Eliminar un elemento Ninguna Ninguna

El siguiente diagrama muestra el diseño básico de la aplicación.

El cliente es todo lo que consume la API web (aplicación móvil, explorador, etcétera). En este tutorial no se
crea ningún cliente. Postman o curl se utilizan como el cliente para probar la aplicación.
Un modelo es un objeto que representa los datos de la aplicación. En este caso, el único modelo es una tarea
pendiente. Los modelos se representan como clases de C#, también conocidas como clases POCO (del
inglés Plain Old C# Object, objetos CRL antiguos sin formato).
Un controlador es un objeto que controla solicitudes HTTP y crea la respuesta HTTP. Esta aplicación tiene un
único controlador.
Para simplificar el tutorial, la aplicación no usa una base de datos persistente. La aplicación de ejemplo
almacena tareas pendientes en una base de datos en memoria.

Requisitos previos
Visual Studio for Windows.
Select the ASP.NET and web development workload.
.Net Core 2.1 SDK

Crear el proyecto
Haga lo siguiente para descargar Visual Studio:
En el menú Archivo, seleccione Nuevo > Proyecto.
Seleccione la plantilla Aplicación web ASP.NET Core. Denomine el proyecto TodoApi y haga clic en Aceptar.
En el cuadro de diálogo Nueva aplicación web ASP.NET Core - TodoApi, seleccione la versión ASP.NET
Core. Seleccione la plantilla API y haga clic en Aceptar. No seleccione Habilitar compatibilidad con Docker.
Iniciar la aplicación
En Visual Studio, presione CTRL+F5 para iniciar la aplicación. Visual Studio inicia un explorador y navega hasta
http://localhost:<port>/api/values , donde <port> es un número de puerto elegido aleatoriamente. En Chrome,
Microsoft Edge y Firefox se muestra la salida siguiente:

["value1","value2"]

Si usa Internet Explorer, se le pedirá que guarde un archivo values.json.


Agregar una clase de modelo
Un modelo es un objeto que representa los datos de la aplicación. En este caso, el único modelo es una tarea
pendiente.
En el Explorador de soluciones, haga clic con el botón derecho en el proyecto. Seleccione Agregar > Nueva
carpeta. Asigne a la carpeta el nombre Models.

NOTE
Las clases del modelo pueden ir en cualquier parte del proyecto. La carpeta Models se usa por convención para las clases de
modelos.

En el Explorador de soluciones, haga clic con el botón derecho en la carpeta de modelos y seleccione Agregar >
Clase. Denomine la clase TodoItem y, después, haga clic en Agregar.
Actualice la clase TodoItem por el siguiente código:
namespace TodoApi.Models
{
public class TodoItem
{
public long Id { get; set; }
public string Name { get; set; }
public bool IsComplete { get; set; }
}
}

La base de datos genera el Id cuando se crea TodoItem .


Crear el contexto de base de datos
El contexto de base de datos es la clase principal que coordina la funcionalidad de Entity Framework para un
modelo de datos determinado. Esta clase se crea derivándola de la clase Microsoft.EntityFrameworkCore.DbContext .
En el Explorador de soluciones, haga clic con el botón derecho en la carpeta de modelos y seleccione Agregar >
Clase. Denomine la clase TodoContext y, después, haga clic en Agregar.
Reemplace la clase por el siguiente código:

using Microsoft.EntityFrameworkCore;

namespace TodoApi.Models
{
public class TodoContext : DbContext
{
public TodoContext(DbContextOptions<TodoContext> options)
: base(options)
{
}

public DbSet<TodoItem> TodoItems { get; set; }


}
}

Registrar el contexto de base de datos


En este paso, el contexto de base de datos se registra con el contenedor de inserción de dependencias. Los servicios
(por ejemplo, el contexto de la base de datos) que se registran con el contenedor de inserción de dependencias (DI)
están disponibles para los controladores.
Registre el contexto de la base de datos con el contenedor de servicio mediante la compatibilidad integrada para
inserción de dependencias. Reemplace el contenido del archivo Startup.cs con el código siguiente:
[!code-csharp]
[!code-csharp]
El código anterior:
Quita el código no usado.
Especifica que se inserte una base de datos en memoria en el contenedor de servicios.
Adición de un controlador
En el Explorador de soluciones, haga clic con el botón derecho en la carpeta Controladores. Seleccione Agregar >
Nuevo elemento. En el cuadro de diálogo Agregar nuevo elemento, seleccione la plantilla Clase de
controlador de API. Denomine la clase TodoController y, después, haga clic en Agregar.
Reemplace la clase por el siguiente código:
[!code-csharp]
El código anterior define una clase de controlador de API sin métodos. En las secciones siguientes, se agregan
métodos para implementar la API.
[!code-csharp]
El código anterior define una clase de controlador de API sin métodos. En las secciones siguientes, se agregan
métodos para implementar la API. La clase se anota con un atributo [ApiController] para habilitar algunas
características muy prácticas. Para más información sobre las características que el atributo habilita, vea Anotación
de una clase con ApiControllerAttribute.
El constructor del controlador usa la inserción de dependencias para insertar el contexto de base de datos (
TodoContext ) en el controlador. El contexto de base de datos se usa en cada uno de los métodos CRUD del
controlador. El constructor agrega un elemento a la base de datos en memoria si no existe ninguno.

Tareas pendientes
Para obtener tareas pendientes, agregue estos métodos a la clase TodoController :
[!code-csharp]
[!code-csharp]
Estos métodos implementan los dos métodos GET:
GET /api/todo
GET /api/todo/{id}

Esta es una respuesta HTTP de ejemplo del método GetAll :


[
{
"id": 1,
"name": "Item1",
"isComplete": false
}
]

Más adelante en el tutorial, veremos cómo se puede ver la respuesta HTTP por medio de Postman o curl.
Enrutamiento y rutas URL
El atributo [HttpGet] indica un método que responde a una solicitud HTTP GET. La ruta de dirección URL para
cada método se construye como sigue:
Tome la cadena de plantilla en el atributo Route del controlador:
[!code-csharp]
[!code-csharp]
Reemplace [controller] por el nombre del controlador, que es el nombre de clase de controlador sin el sufijo
"Controller". En este ejemplo, el nombre de clase de controlador es TodoController y el nombre de raíz es
"todo". El enrutamiento en ASP.NET Core no distingue entre mayúsculas y minúsculas.
Si el atributo [HttpGet] tiene una plantilla de ruta (como [HttpGet("/products")] ), anexiónela a la ruta de
acceso. En este ejemplo no se usa una plantilla. Para más información, vea Enrutamiento mediante atributos con
atributos Http[Verb].
En el siguiente método GetById , "{id}" es una variable de marcador de posición correspondiente al identificador
único de la tarea pendiente. Cuando GetById se invoca, asigna el valor "{id}" de la dirección URL al parámetro
id del método.

[!code-csharp]
[!code-csharp]
Name = "GetTodo" crea una ruta con nombre. Rutas con nombre:
Permita que la aplicación cree un vínculo HTTP mediante el nombre de ruta.
Se explican más adelante en el tutorial.
Valores devueltos
El método GetAll devuelve una colección de objetos TodoItem . MVC serializa automáticamente el objeto a JSON
y escribe el JSON en el cuerpo del mensaje de respuesta. El código de respuesta para este método es 200,
suponiendo que no haya ninguna excepción no controlada. Las excepciones no controladas se convierten en
errores 5xx.
En cambio, el método GetById devuelve el tipo más general IActionResult, que representa una amplia gama de
tipos de valor devuelto. GetById tiene dos tipos de valor devueltos distintos:
Si no hay ningún elemento que coincida con el identificador solicitado, el método devuelve un error 404.
Devolver NotFound genera una respuesta HTTP 404.
En caso contrario, el método devuelve 200 con un cuerpo de respuesta JSON. Devolver Ok genera una
respuesta HTTP 200.
En cambio, el método GetById devuelve el tipo ActionResult<T>, que representa una amplia gama de tipos de
valor devuelto. GetById tiene dos tipos de valor devueltos distintos:
Si no hay ningún elemento que coincida con el identificador solicitado, el método devuelve un error 404.
Devolver NotFound genera una respuesta HTTP 404.
En caso contrario, el método devuelve 200 con un cuerpo de respuesta JSON. Devolver item genera una
respuesta HTTP 200.
Iniciar la aplicación
En Visual Studio, presione CTRL+F5 para iniciar la aplicación. Visual Studio inicia un explorador y navega hasta
http://localhost:<port>/api/values , donde <port> es un número de puerto elegido aleatoriamente. Vaya al
controlador Todo en http://localhost:<port>/api/todo .

Implementar las otras operaciones CRUD


En las secciones siguientes, se agregan los métodos Create , Update y Delete al controlador.
Crear
Agregue el siguiente método Create :
[!code-csharp]
El código anterior es un método HTTP POST, según indica el atributo [HttpPost]. El atributo [FromBody] indica a
MVC que obtenga el valor de la tarea pendiente del cuerpo de la solicitud HTTP.
[!code-csharp]
El código anterior es un método HTTP POST, según indica el atributo [HttpPost]. MVC obtiene el valor de la tarea
pendiente del cuerpo de la solicitud HTTP.
El método CreatedAtRoute realiza las acciones siguientes:
Devuelve una respuesta 201. HTTP 201 es la respuesta estándar para un método HTTP POST que crea un
recurso en el servidor.
Agrega un encabezado de ubicación a la respuesta. El encabezado de ubicación especifica el URI de la tarea
pendiente recién creada. Vea 10.2.2 201 Created (10.2.2 201 creada).
Usa la ruta denominada "GetTodo" para crear la dirección URL. La ruta con nombre "GetTodo" se define en
GetById :

[!code-csharp]
[!code-csharp]
Usar Postman para enviar una solicitud de creación
Inicia la aplicación.
Abra Postman.
Actualice el número de puerto en la dirección URL de localhost.
Establezca el método HTTP en POST.
Haga clic en la pestaña Body (Cuerpo).
Seleccione el botón de radio Raw (Sin formato).
Establezca el tipo en JSON (application/json).
Escriba un cuerpo de solicitud con una tarea pendiente parecida al siguiente JSON:

{
"name":"walk dog",
"isComplete":true
}

Haga clic en el botón Send (Enviar).

TIP
Si no aparece ninguna respuesta tras hacer clic en Send (Enviar), deshabilite la opción SSL certification verification
(Comprobación de certificación SSL). La encontrará en File > Settings (Archivo > Configuración). Vuelva a hacer clic en el
botón Send (Enviar) después de deshabilitar la configuración.

Haga clic en la pestaña Headers (Encabezados) del panel Response (Respuesta) y copie el valor de encabezado de
Location (Ubicación):
El URI del encabezado de ubicación puede utilizarse para acceder al nuevo elemento.
Actualizar
Agregue el siguiente método Update :
[!code-csharp]
[!code-csharp]
Update es similar a Create , salvo por el hecho de que usa HTTP PUT. La respuesta es 204 Sin contenido. Según la
especificación HTTP, una solicitud PUT requiere que el cliente envíe toda la entidad actualizada, no solo los deltas.
Para admitir actualizaciones parciales, use HTTP PATCH.
Use Postman para actualizar el nombre de la tarea pendiente a "walk cat":
Eliminar
Agregue el siguiente método Delete :

[HttpDelete("{id}")]
public IActionResult Delete(long id)
{
var todo = _context.TodoItems.Find(id);
if (todo == null)
{
return NotFound();
}

_context.TodoItems.Remove(todo);
_context.SaveChanges();
return NoContent();
}

La respuesta de Delete es 204 Sin contenido.


Use Postman para eliminar la tarea pendiente:
Llamar a Web API con jQuery
En esta sección, se agrega una página HTML que usa jQuery para llamar a Web API. jQuery inicia la solicitud y
actualiza la página con los detalles de la respuesta de la API.
Configure el proyecto para atender archivos estáticos y para permitir la asignación de archivos predeterminada.
Esto se logra invocando los métodos de extensión UseStaticFiles y UseDefaultFiles en Startup.Configure. Para
obtener más información, consulte Archivos estáticos.

public void Configure(IApplicationBuilder app)


{
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseMvc();
}

Agregue un archivo HTML denominado index.html al directorio wwwroot del proyecto. Reemplace el contenido
por el siguiente marcado:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>To-do CRUD</title>
<style>
input[type='submit'], button, [aria-label] {
cursor: pointer;
}
}

#spoiler {
display: none;
}

table {
font-family: Arial, sans-serif;
border: 1px solid;
border-collapse: collapse;
}

th {
background-color: #0066CC;
color: white;
}

td {
border: 1px solid;
padding: 5px;
}
</style>
</head>
<body>
<h1>To-do CRUD</h1>
<h3>Add</h3>
<form action="javascript:void(0);" method="POST" onsubmit="addItem()">
<input type="text" id="add-name" placeholder="New to-do">
<input type="submit" value="Add">
</form>

<div id="spoiler">
<h3>Edit</h3>
<form class="my-form">
<input type="hidden" id="edit-id">
<input type="checkbox" id="edit-isComplete">
<input type="text" id="edit-name">
<input type="submit" value="Edit">
<a onclick="closeInput()" aria-label="Close">&#10006;</a>
</form>
</div>

<p id="counter"></p>

<table>
<tr>
<th>Is Complete</th>
<th>Name</th>
<th></th>
<th></th>
</tr>
<tbody id="todos"></tbody>
</table>

<script src="https://code.jquery.com/jquery-3.3.1.min.js"
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
crossorigin="anonymous"></script>
<script src="site.js"></script>
</body>
</html>

Agregue un archivo JavaScript denominado site.js al directorio wwwroot del proyecto. Reemplace el contenido por
el siguiente código:

const uri = 'api/todo';


let todos = null;
function getCount(data) {
const el = $('#counter');
const el = $('#counter');
let name = 'to-do';
if (data) {
if (data > 1) {
name = 'to-dos';
}
el.text(data + ' ' + name);
} else {
el.html('No ' + name);
}
}

$(document).ready(function () {
getData();
});

function getData() {
$.ajax({
type: 'GET',
url: uri,
success: function (data) {
$('#todos').empty();
getCount(data.length);
$.each(data, function (key, item) {
const checked = item.isComplete ? 'checked' : '';

$('<tr><td><input disabled="true" type="checkbox" ' + checked + '></td>' +


'<td>' + item.name + '</td>' +
'<td><button onclick="editItem(' + item.id + ')">Edit</button></td>' +
'<td><button onclick="deleteItem(' + item.id + ')">Delete</button></td>' +
'</tr>').appendTo($('#todos'));
});

todos = data;
}
});
}

function addItem() {
const item = {
'name': $('#add-name').val(),
'isComplete': false
};

$.ajax({
type: 'POST',
accepts: 'application/json',
url: uri,
contentType: 'application/json',
data: JSON.stringify(item),
error: function (jqXHR, textStatus, errorThrown) {
alert('here');
},
success: function (result) {
getData();
$('#add-name').val('');
}
});
}

function deleteItem(id) {
$.ajax({
url: uri + '/' + id,
type: 'DELETE',
success: function (result) {
getData();
}
});
}
function editItem(id) {
$.each(todos, function (key, item) {
if (item.id === id) {
$('#edit-name').val(item.name);
$('#edit-id').val(item.id);
$('#edit-isComplete').val(item.isComplete);
}
});
$('#spoiler').css({ 'display': 'block' });
}

$('.my-form').on('submit', function () {
const item = {
'name': $('#edit-name').val(),
'isComplete': $('#edit-isComplete').is(':checked'),
'id': $('#edit-id').val()
};

$.ajax({
url: uri + '/' + $('#edit-id').val(),
type: 'PUT',
accepts: 'application/json',
contentType: 'application/json',
data: JSON.stringify(item),
success: function (result) {
getData();
}
});

closeInput();
return false;
});

function closeInput() {
$('#spoiler').css({ 'display': 'none' });
}

Puede que sea necesario realizar un cambio en la configuración de inicio del proyecto de ASP.NET Core para
comprobar la página HTML localmente. Abra launchSettings.json en el directorio Properties del proyecto. Quite la
propiedad launchUrl para forzar a la aplicación a abrirse en index.html, esto es, el archivo predeterminado del
proyecto.
Existen varias formas de obtener jQuery. En el fragmento de código anterior, la biblioteca se carga desde una red
CDN. Este ejemplo es un ejemplo de CRUD completo de llamada a la API de jQuery. Existen más características en
este ejemplo para hacer que la experiencia sea más completa. Aquí verá algunas explicaciones sobre las llamadas a
la API.
Obtener una lista de tareas pendientes
Para obtener una lista de tareas pendientes, envíe una solicitud HTTP GET a /api/todo.
La función de JQuery ajax envía una solicitud AJAX a la API, que devuelve código JSON que representa un objeto
o una matriz. Esta función puede controlar todas las formas de interacción de HTTP enviando una solicitud HTTP a
la url especificada. GET se emite como type . La función de devolución de llamada success se invoca si la
solicitud se realiza correctamente. En la devolución de llamada, el DOM se actualiza con la información de la tarea
pendiente.
$(document).ready(function () {
getData();
});

function getData() {
$.ajax({
type: 'GET',
url: uri,
success: function (data) {
$('#todos').empty();
getCount(data.length);
$.each(data, function (key, item) {
const checked = item.isComplete ? 'checked' : '';

$('<tr><td><input disabled="true" type="checkbox" ' + checked + '></td>' +


'<td>' + item.name + '</td>' +
'<td><button onclick="editItem(' + item.id + ')">Edit</button></td>' +
'<td><button onclick="deleteItem(' + item.id + ')">Delete</button></td>' +
'</tr>').appendTo($('#todos'));
});

todos = data;
}
});
}

Agregar una tarea pendiente


Para agregar una tarea pendiente, envíe una solicitud HTTP POST a /api/todo. El cuerpo de la solicitud debe
contener un objeto de tarea pendiente. La función ajax usa POST para llamar a la API. En las solicitudes POST y
PUT , el cuerpo de la solicitud representa los datos enviados a la API. La API espera un cuerpo de solicitud con
formato JSON. Las opciones accepts y contentType se establecen en application/json para clasificar el tipo de
medio que se va a recibir y a enviar respectivamente. Los datos se convierten en un objeto JSON usando
JSON.stringify . Cuando la API devuelve un código de estado correcto, se invoca la función getData para
actualizar la tabla HTML.

function addItem() {
const item = {
'name': $('#add-name').val(),
'isComplete': false
};

$.ajax({
type: 'POST',
accepts: 'application/json',
url: uri,
contentType: 'application/json',
data: JSON.stringify(item),
error: function (jqXHR, textStatus, errorThrown) {
alert('here');
},
success: function (result) {
getData();
$('#add-name').val('');
}
});
}

Actualizar una tarea pendiente


Actualizar una tarea pendiente es muy parecido a agregarla, ya que ambos procesos se basan en el cuerpo de
solicitud. En este caso, la única diferencia real entre ambos es que url cambia para reflejar el identificador único
del elemento, y type es PUT .

$.ajax({
url: uri + '/' + $('#edit-id').val(),
type: 'PUT',
accepts: 'application/json',
contentType: 'application/json',
data: JSON.stringify(item),
success: function (result) {
getData();
}
});

Eliminar una tarea pendiente


Para eliminar una tarea pendiente, hay que establecer type de la llamada de AJAX en DELETE y especificar el
identificador único de la tarea en la dirección URL.

$.ajax({
url: uri + '/' + id,
type: 'DELETE',
success: function (result) {
getData();
}
});

Pasos siguientes
Para más información sobre cómo usar una base de datos persistente, vea:
Creación de una aplicación web de páginas de Razor con ASP.NET Core
Trabajo con datos en ASP.NET Core
Páginas de ayuda de ASP.NET Core Web API mediante Swagger
Enrutamiento a acciones del controlador
Compilación de API web con ASP.NET Core
Tipos de valor devuelto de acción del controlador
Para obtener información sobre la implementación de una API, como en Azure App Service, vea la
documentación sobre Hospedaje e implementación.
Vea o descargue el código de ejemplo. Vea cómo descargarlo.
Tutoriales de ASP.NET Core
12/06/2018 • 3 minutes to read • Edit Online

Están disponibles las siguientes guías detalladas para desarrollar aplicaciones de ASP.NET Core:

Compilación de aplicaciones web


Las páginas de Razor son el método recomendado para crear una aplicación de interfaz de usuario web con
ASP.NET Core 2.0.
Introducción a las páginas de Razor en ASP.NET Core
Creación de una aplicación web de páginas de Razor con ASP.NET Core
Páginas de Razor en Windows
Páginas de Razor en macOS
Páginas de Razor con VSCode
Creación de una aplicación web de ASP.NET Core MVC
Aplicación web con Visual Studio para Windows
Aplicación web con Visual Studio para Mac
Aplicación web con Visual Studio Code en macOS o Linux
Introducción a ASP.NET Core y Entity Framework Core con Visual Studio
Creación de aplicaciones auxiliares de etiquetas
Creación de un componente de vista simple
Desarrollo de aplicaciones mediante un monitor de archivos

Compilación de API web


Creación de una API web con ASP.NET Core
API web con Visual Studio para Windows
API web con Visual Studio para Mac
API web con Visual Studio Code
Páginas de ayuda de ASP.NET Core Web API mediante Swagger
Get started with NSwag (Introducción a NSwag)
Get started with Swashbuckle (Introducción a Swashbuckle)
Creación de servicios web de back-end para aplicaciones móviles nativas

Acceso a datos y almacenamiento


Introducción a las páginas de Razor y Entity Framework Core con Visual Studio
Introducción a ASP.NET Core MVC y Entity Framework Core con Visual Studio
ASP.NET Core MVC con EF Core: nueva base de datos
ASP.NET Core MVC con EF Core: base de datos existente

Autenticación y autorización
Habilitar la autenticación con Facebook, Google y otros proveedores externos
Confirmación de cuentas y recuperación de contraseñas
Autenticación en dos fases con SMS

Desarrollo del lado cliente


Uso de Gulp
Uso de Grunt
Administración de paquetes de cliente con Bower
Creación de sitios con capacidad de respuesta con Bootstrap

Prueba
Pruebas unitarias de .NET Core mediante dotnet test

Hospedaje e implementación
Implementar una aplicación web de ASP.NET Core en Azure con Visual Studio
Implementar una aplicación web de ASP.NET Core en Azure con la línea de comandos
Publicación en una aplicación web de Azure con una implementación continua
Implementar un contenedor de ASP.NET en un host remoto de Docker
ASP.NET Core y Azure Service Fabric

Cómo descargar un ejemplo


1. Descargue el archivo ZIP del repositorio de ASP.NET.
2. Descomprima el archivo Docs-master.zip.
3. Use la dirección URL del vínculo de ejemplo para ir al directorio de ejemplo.
Creación de una aplicación web de páginas de Razor
con ASP.NET Core
21/06/2018 • 2 minutes to read • Edit Online

En esta serie se explican los conceptos básicos de la creación de una aplicación web de páginas de Razor con
ASP.NET Core mediante Visual Studio. Otras versiones de esta serie incluyen una versión para macOS y una
versión de Visual Studio Code.
1. Introducción a las páginas de Razor
2. Adición de un modelo a una aplicación de páginas de Razor
3. Páginas de Razor con scaffolding
4. Trabajar con SQL Server LocalDB
5. Actualización de páginas
6. Agregar búsqueda
7. Agregar un campo nuevo
8. Agregar validación
9. Carga de archivos
Introducción a las páginas de Razor en ASP.NET
Core
17/05/2018 • 6 minutes to read • Edit Online

Por Rick Anderson


En este tutorial se enseñan los conceptos básicos de la compilación de una aplicación web de páginas de
Razor de ASP.NET Core. Las páginas de Razor son el método recomendado para crear la interfaz de usuario
de aplicaciones web en ASP.NET Core.
Hay tres versiones de este tutorial:
Windows: este tutorial
MacOS: Introducción a las páginas de Razor en ASP.NET Core con Visual Studio para Mac
macOS, Linux y Windows: Introducción a las páginas de Razor en ASP.NET Core con Visual Studio Code
Vea o descargue el código de ejemplo (cómo descargarlo)

Requisitos previos
Visual Studio for Windows.
Select the ASP.NET and web development workload.
.Net Core 2.1 SDK

Creación de una aplicación web de Razor


En el menú Archivo de Visual Studio, seleccione Nuevo > Proyecto.
Cree una aplicación web de ASP.NET Core. Asigne al proyecto el nombre RazorPagesMovie. Es
importante asignarle el nombre RazorPagesMovie para que los espacios de nombres coincidan al
copiar y pegar el código.
Seleccione ASP.NET Core 2.0 en la lista desplegable y, luego, seleccione Aplicación web.

NOTE
Para usar ASP.NET Core con .NET Framework, primero debe seleccionar .NET Framework en la lista desplegable
situada en el cuadro de diálogo de la izquierda y luego puede seleccionar la versión de ASP.NET Core deseada.
La plantilla de Visual Studio crea un proyecto de inicio:

Presione F5 para ejecutar la aplicación en modo de depuración o Ctrl-F5 para que se ejecute sin adjuntar el
depurador.
Visual Studio inicia IIS Express y ejecuta la aplicación. En la barra de direcciones aparece localhost:port#
(y no algo como example.com ). Esto es así porque localhost es el nombre de host estándar del equipo
local. Localhost solo sirve las solicitudes web del equipo local. Cuando Visual Studio crea un proyecto web,
se usa un puerto aleatorio para el servidor web. En la imagen anterior, el número de puerto es 5000. Al
ejecutar la aplicación verá otro puerto distinto.
Iniciar la aplicación con CTRL+F5 (modo de no depuración) le permite efectuar cambios en el código,
guardar el archivo, actualizar el explorador y ver los cambios de código. Muchos desarrolladores prefieren
usar el modo de no depuración para iniciar la aplicación rápidamente y ver los cambios.
La plantilla predeterminada crea los vínculos y las páginas RazorPagesMovie, Inicio, Sobre y Contacto.
Según el tamaño de la ventana del explorador, tendrá que hacer clic en el icono de navegación para mostrar
los vínculos.
Pruebe los vínculos. Los vínculos RazorPagesMovie e Inicio dirigen a la página de índice. Los vínculos
Acerca de y Contacto dirigen a las páginas About y Contact respectivamente.

Archivos y carpetas del proyecto


En la tabla siguiente se enumeran los archivos y las carpetas del proyecto. En este tutorial, el archivo Startup.cs
es el más importante. No es necesario revisar todos los vínculos siguientes. Los vínculos se proporcionan
como referencia para cuando necesite más información sobre un archivo o una carpeta del proyecto.

ARCHIVO O CARPETA PROPÓSITO

wwwroot Contiene archivos estáticos. Vea Archivos estáticos.

Páginas Carpeta para páginas de Razor.

appsettings.json Configuración

Program.cs Aloja la aplicación de ASP.NET Core.

Startup.cs Configura los servicios y la canalización de solicitudes.


Consulte Inicio.

Carpeta Páginas
El archivo _Layout.cshtml contiene elementos HTML comunes (scripts y hojas de estilos) y establece el diseño
de la aplicación. Por ejemplo, al hacer clic en RazorPagesMovie, Inicio, Acerca de o Contacto, verá los
mismos elementos. Los elementos comunes incluyen el menú de navegación de la parte superior y el
encabezado de la parte inferior de la ventana. Consulte Diseño para obtener más información.
El archivo _ViewStart.cshtml establece la propiedad Layout de las páginas de Razor que para usar el archivo
_Layout.cshtml. Vea Layout (Diseño) para más información.
El archivo _ViewImports.cshtml contiene directivas de Razor que se importan en cada página de Razor.
Consulte Importing Shared Directives (Importar directivas compartidas) para obtener más información.
El archivo _ValidationScriptsPartial.cshtml proporciona una referencia de los scripts de validación de jQuery.
Al agregar las páginas Create y Edit más adelante en el tutorial, se usará el archivo
_ValidationScriptsPartial.cshtml.
Las páginas About , Contact y Index son páginas básicas que puede usar para iniciar una aplicación. La
página Error se usa para mostrar información de errores.

S IG U IE N T E : A D IC IÓ N D E U N
M ODELO
Agregar un modelo a una aplicación de páginas de
Razor en ASP.NET Core
25/05/2018 • 7 minutes to read • Edit Online

Por Rick Anderson


En esta sección, agregará las clases para administrar películas en una base de datos. Estas clases se usan con
Entity Framework Core (EF Core) para trabajar con una base de datos. EF Core es un marco de trabajo de
asignación relacional de objetos (ORM ) que simplifica el código de acceso de datos que se debe escribir.
Las clases de modelo que se crean se conocen como clases POCO (del inglés "plain-old CLR objects", objetos
CLR antiguos sin formato) porque no tienen ninguna dependencia de EF Core. Definen las propiedades de los
datos que se almacenan en la base de datos.
En este tutorial, se escriben primero las clases del modelo y EF Core crea la base de datos. Hay un enfoque
alternativo, que no se trata aquí, que consiste en generar clases del modelo a partir de una base de datos existente.
Vea o descargue un ejemplo.

Agregar un modelo de datos


En el Explorador de soluciones, haga clic con el botón derecho en el proyecto RazorPagesMovie > Agregar >
Nueva carpeta. Asigne a la carpeta el nombre Models.
Haga clic con el botón derecho en la carpeta Models. Seleccione Agregar > Clase. Asigne a la clase el nombre
Movie y agregue las siguientes propiedades:
Agregue las propiedades siguientes a la clase Movie :

using System;

namespace RazorPagesMovie.Models
{
public class Movie
{
public int ID { get; set; }
public string Title { get; set; }
public DateTime ReleaseDate { get; set; }
public string Genre { get; set; }
public decimal Price { get; set; }
}
}

La base de datos requiere el campo ID para la clave principal.


Agregar una clase de contexto de base de datos
Agregue la clase MovieContext.cs siguiente a la carpeta Models:
using Microsoft.EntityFrameworkCore;

namespace RazorPagesMovie.Models
{
public class MovieContext : DbContext
{
public MovieContext(DbContextOptions<MovieContext> options)
: base(options)
{
}

public DbSet<Movie> Movie { get; set; }


}
}

El código anterior crea una propiedad DbSet para el conjunto de entidades. En la terminología de Entity
Framework, un conjunto de entidades suele corresponderse con una tabla de base de datos, mientras que una
entidad lo hace con una fila de la tabla.
Agregar una cadena de conexión de base de datos
Agregue una cadena de conexión al archivo appsettings.json.

{
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Warning"
}
},
"ConnectionStrings": {
"MovieContext": "Server=(localdb)\\mssqllocaldb;Database=Movie-
1;Trusted_Connection=True;MultipleActiveResultSets=true"
}
}

Registrar el contexto de base de datos


Registre el contexto de base de datos con el contenedor de inserción de dependencias en el método
ConfigureServices de la clase Startup (Startup.cs):

public void ConfigureServices(IServiceCollection services)


{
// requires
// using RazorPagesMovie.Models;
// using Microsoft.EntityFrameworkCore;

services.AddDbContext<MovieContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("MovieContext")));
services.AddMvc();
}

Compile el proyecto para comprobar que no contiene errores.

Agregar herramientas de scaffolding y realizar la migración inicial


En esta sección, usará la Consola del Administrador de paquetes (PMC ) para:
Agregar el paquete de generación de código de Visual Studio web. Este paquete es necesario para ejecutar el
motor de scaffolding.
Agregar una migración inicial.
Actualizar la base de datos con la migración inicial.
En el menú Herramientas, seleccione Administrador de paquetes NuGet > Consola del Administrador de
paquetes.

En PCM, escriba los siguientes comandos:

Install-Package Microsoft.VisualStudio.Web.CodeGeneration.Design -Version 2.0.3


Add-Migration Initial
Update-Database

Se pueden usar los siguientes comandos de la CLI de .NET Core de forma alternativa:

dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design


dotnet ef migrations add Initial
dotnet ef database update

El comando Install-Package instala las herramientas necesarias para ejecutar el motor de scaffolding.
El comando Add-Migration genera el código para crear el esquema de base de datos inicial. El esquema se basa
en el modelo especificado en DbContext (en el archivo Models/MovieContext.cs). El argumento Initial se usa
para asignar un nombre a las migraciones. Puede usar cualquier nombre, pero se suele elegir uno que describa la
migración. Vea Introduction to migrations (Introducción a las migraciones) para obtener más información.
El comando Update-Database ejecuta el método Up en el archivo Migrations/<time-stamp>_InitialCreate.cs, con
lo que se crea la base de datos.
Aplicar scaffolding al modelo de película
Ejecute lo siguiente desde la línea de comandos (en el directorio del proyecto que contiene los archivos
Program.cs, Startup.cs y .csproj):

dotnet aspnet-codegenerator razorpage -m Movie -dc MovieContext -udl -outDir Pages\Movies --


referenceScriptLibraries

Si se produce un error:
No executable found matching command "dotnet-aspnet-codegenerator"

El error anterior se produce cuando el usuario se encuentra en el directorio incorrecto. Abra un shell de comandos
en el directorio del proyecto (el directorio que contiene los archivos Program.cs, Startup.cs y .csproj) y, después,
ejecute el comando anterior.
Si se produce un error:

The process cannot access the file


'RazorPagesMovie/bin/Debug/netcoreapp2.0/RazorPagesMovie.dll'
because it is being used by another process.

Salga de Visual Studio y vuelva a ejecutar el comando.


En la tabla siguiente se incluyen los detalles de los parámetros de los generadores de código de ASP.NET Core:

PARÁMETRO DESCRIPTION

-m Nombre del modelo

-dc Contexto de datos

-udl Usa el diseño predeterminado.

-outDir Ruta de acceso relativa de la carpeta de salida para crear las


vistas

--referenceScriptLibraries Agrega _ValidationScriptsPartial a las páginas Editar y


Crear.

Active o desactive h para obtener ayuda con el comando aspnet-codegenerator razorpage :

dotnet aspnet-codegenerator razorpage -h

Prueba de la aplicación
Ejecute la aplicación y anexe /Movies a la dirección URL en el explorador ( http://localhost:port/movies ).
Pruebe el vínculo Crear.
Pruebe los vínculos Editar, Detalles y Eliminar.
Si obtiene una excepción SQL, verifique que haya ejecuto las migraciones y que la base de datos esté actualizada:
En el tutorial siguiente se explican los archivos creados mediante scaffolding.

A N T E R IO R : S IG U IE N T E : P Á G IN A S D E R A Z O R C R E A D A S M E D IA N T E
IN T R O D U C C IÓ N S C A F F O L D IN G
Páginas de Razor con scaffolding en ASP.NET Core
25/06/2018 • 12 minutes to read • Edit Online

Por Rick Anderson


En este tutorial se examinan las páginas de Razor creadas por la técnica scaffolding en el tutorial anterior.
Vea o descargue un ejemplo.

Páginas Crear, Eliminar, Detalles y Editar.


Examine el modelo de página Pages/Movies/Index.cshtml.cs: [!code-csharp]
Las páginas de Razor se derivan de PageModel . Por convención, la clase derivada de PageModel se denomina
<PageName>Model . El constructor aplica la inserción de dependencias para agregar el MovieContext a la página.
Todas las páginas con scaffolding siguen este patrón. Vea Código asincrónico para obtener más información sobre
programación asincrónica con Entity Framework.
Cuando se efectúa una solicitud para la página, el método OnGetAsync devuelve una lista de películas a la página
de Razor. Se llama a OnGetAsync o a OnGet en una página de Razor para inicializar el estado de la página. En este
caso, OnGetAsync obtiene una lista de películas y las muestra.
Cuando OnGet devuelve void o OnGetAsync devuelve Task , no se utiliza ningún método de devolución. Cuando
el tipo de valor devuelto es IActionResult o Task<IActionResult> , se debe proporcionar una instrucción return.
Por ejemplo, el método Pages/Movies/Create.cshtml.cs OnPostAsync :

public async Task<IActionResult> OnPostAsync()


{
if (!ModelState.IsValid)
{
return Page();
}

_context.Movie.Add(Movie);
await _context.SaveChangesAsync();

return RedirectToPage("./Index");
}

Examine la página de Razor Pages/Movies/Index.cshtml:


@page
@model RazorPagesMovie.Pages.Movies.IndexModel

@{
ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
<a asp-page="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Movie[0].Title)
</th>
<th>
@Html.DisplayNameFor(model => model.Movie[0].ReleaseDate)
</th>
<th>
@Html.DisplayNameFor(model => model.Movie[0].Genre)
</th>
<th>
@Html.DisplayNameFor(model => model.Movie[0].Price)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Movie) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.ReleaseDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Genre)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
<a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>

Razor puede realizar la transición de HTML a C# o a un marcado específico de Razor. Cuando el símbolo @ va
seguido de una palabra clave reservada de Razor, realiza una transición a un marcado específico de Razor; en
caso contrario, realiza la transición a C#.
La directiva de Razor @page convierte el archivo en una acción — de MVC, lo que significa que puede controlar
las solicitudes. @page debe ser la primera directiva de Razor de una página. @page es un ejemplo de la transición
a un marcado específico de Razor. Vea Razor syntax (Sintaxis de Razor) para más información.
Examine la expresión lambda usada en la siguiente aplicación auxiliar HTML:
@Html.DisplayNameFor(model => model.Movie[0].Title))

La aplicación auxiliar HTML DisplayNameFor inspecciona la propiedad Title a la que se hace referencia en la
expresión lambda para determinar el nombre para mostrar. La expresión lambda se inspecciona, no se evalúa.
Esto significa que no hay ninguna infracción de acceso si model , model.Movie o model.Movie[0] son null o
están vacíos. Al evaluar la expresión lambda (por ejemplo, con @Html.DisplayFor(modelItem => item.Title) ), se
evalúan los valores de propiedad del modelo.
La directiva @model

@page
@model RazorPagesMovie.Pages.Movies.IndexModel

La directiva @model especifica el tipo del modelo que se pasa a la página de Razor. En el ejemplo anterior, la línea
@model permite que la clase derivada de PageModel esté disponible en la página de Razor. El modelo se usa en las
aplicaciones auxiliares HTML @Html.DisplayNameFor y @Html.DisplayName de la página.
Propiedades ViewData y Layout
Observe el código siguiente:

@page
@model RazorPagesMovie.Pages.Movies.IndexModel

@{
ViewData["Title"] = "Index";
}

El código resaltado anterior es un ejemplo de Razor con una transición a C#. Los caracteres { y } delimitan un
bloque de código de C#.
La clase base PageModel tiene una propiedad de diccionario ViewData que se puede usar para agregar datos que
quiera pasar a una vista. Puede agregar objetos al diccionario ViewData con un patrón de clave/valor. En el
ejemplo anterior, se agrega la propiedad "Title" al diccionario ViewData . La propiedad "Title" se usa en el archivo
Pages/_Layout.cshtml. En el siguiente marcado se muestran las primeras líneas del archivo Pages/_Layout.cshtml.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - RazorPagesMovie</title>

@*Markup removed for brevity.*@

La línea @*Markup removed for brevity.*@ es un comentario de Razor. A diferencia de los comentarios HTML (
<!-- --> ), los comentarios de Razor no se envían al cliente.

Ejecute la aplicación y pruebe los vínculos del proyecto (Inicio, Acerca de, Contacto, Crear, Editar y Eliminar).
Cada página establece el título, que puede ver en la pestaña del explorador. Al marcar una página, se usa el título
para el marcador. Pages/Index.cshtml y Pages/Movies/Index.cshtml actualmente tienen el mismo título, pero
puede modificarlos para que tengan valores diferentes.
La propiedad Layout se establece en el archivo Pages/_ViewStart.cshtml:
@{
Layout = "_Layout";
}

El marcado anterior establece el archivo de diseño en Pages/_Layout.cshtml para todos los archivos de Razor en la
carpeta Pages. Vea Layout (Diseño) para más información.
Actualizar el diseño
Cambie el elemento <title> del archivo Pages/_Layout.cshtml para usar una cadena más corta.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - Movie</title>

Busque el siguiente elemento delimitador en el archivo Pages/_Layout.cshtml.

<a asp-page="/Index" class="navbar-brand">RazorPagesMovie</a>

Reemplace el elemento anterior por el marcado siguiente.

<a asp-page="/Movies/Index" class="navbar-brand">RpMovie</a>

El elemento delimitador anterior es una aplicación auxiliar de etiquetas. En este caso, se trata de la aplicación
auxiliar de etiquetas Anchor. El atributo y valor de la aplicación auxiliar de etiquetas asp-page="/Movies/Index"
crea un vínculo a la página de Razor /Movies/Index .
Guarde los cambios y pruebe la aplicación haciendo clic en el vínculo RpMovie. Consulte el archivo
_Layout.cshtml en GitHub.
Modelo de página Crear
Examine el modelo de página Pages/Movies/Create.cshtml.cs:
// Unused usings removed.
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesMovie.Models;
using System.Threading.Tasks;

namespace RazorPagesMovie.Pages.Movies
{
public class CreateModel : PageModel
{
private readonly RazorPagesMovie.Models.MovieContext _context;

public CreateModel(RazorPagesMovie.Models.MovieContext context)


{
_context = context;
}

public IActionResult OnGet()


{
return Page();
}

[BindProperty]
public Movie Movie { get; set; }

public async Task<IActionResult> OnPostAsync()


{
if (!ModelState.IsValid)
{
return Page();
}

_context.Movie.Add(Movie);
await _context.SaveChangesAsync();

return RedirectToPage("./Index");
}
}
}

El método OnGet inicializa cualquier estado necesario para la página. La página Crear no tiene ningún estado
para inicializar. El método Page crea un objeto PageResult que representa la página Create.cshtml.
La propiedad Movie usa el atributo [BindProperty] para participar en el enlace de modelos. Cuando el
formulario de creación publica los valores del formulario, el tiempo de ejecución de ASP.NET Core enlaza los
valores publicados con el modelo Movie .
El método OnPostAsync se ejecuta cuando la página publica los datos del formulario:

public async Task<IActionResult> OnPostAsync()


{
if (!ModelState.IsValid)
{
return Page();
}

_context.Movie.Add(Movie);
await _context.SaveChangesAsync();

return RedirectToPage("./Index");
}

Si hay algún error de modelo, se vuelve a mostrar el formulario, junto con los datos del formulario publicados. La
mayoría de los errores de modelo se pueden capturar en el cliente antes de que se publique el formulario. Un
ejemplo de un error de modelo consiste en publicar un valor para el campo de fecha que no se puede convertir en
una fecha. Más adelante en el tutorial volveremos a hablar de la validación del lado cliente y de la validación de
modelos.
Si no hay ningún error de modelo, los datos se guardan y el explorador se redirige a la página Índice.
Página de Razor Crear
Examine el archivo de la página de Razor Pages/Movies/Create.cshtml:

@page
@model RazorPagesMovie.Pages.Movies.CreateModel

@{
ViewData["Title"] = "Create";
}

<h2>Create</h2>

<h4>Movie</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Movie.Title" class="control-label"></label>
<input asp-for="Movie.Title" class="form-control" />
<span asp-validation-for="Movie.Title" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Movie.ReleaseDate" class="control-label"></label>
<input asp-for="Movie.ReleaseDate" class="form-control" />
<span asp-validation-for="Movie.ReleaseDate" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Movie.Genre" class="control-label"></label>
<input asp-for="Movie.Genre" class="form-control" />
<span asp-validation-for="Movie.Genre" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Movie.Price" class="control-label"></label>
<input asp-for="Movie.Price" class="form-control" />
<span asp-validation-for="Movie.Price" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</form>
</div>
</div>

<div>
<a asp-page="Index">Back to List</a>
</div>

@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

Visual Studio muestra la etiqueta <form method="post"> con una fuente diferenciada que se aplica a las
aplicaciones auxiliares de etiquetas:
El elemento <form method="post"> es una aplicación auxiliar de etiquetas de formulario. La aplicación auxiliar de
etiquetas de formulario incluye automáticamente un token antifalsificación.
El motor de scaffolding crea un marcado de Razor para cada campo del modelo (excepto el identificador) similar
al siguiente:

<div asp-validation-summary="ModelOnly" class="text-danger"></div>


<div class="form-group">
<label asp-for="Movie.Title" class="control-label"></label>
<input asp-for="Movie.Title" class="form-control" />
<span asp-validation-for="Movie.Title" class="text-danger"></span>
</div>

Las aplicaciones auxiliares de etiquetas de validación ( <div asp-validation-summary y <span asp-validation-for )


muestran errores de validación. La validación se trata con más detalle en un punto posterior de esta serie.
La aplicación auxiliar de etiquetas ( <label asp-for="Movie.Title" class="control-label"></label> ) genera el título
de la etiqueta y el atributo for para la propiedad Title .
La aplicación auxiliar de etiquetas de entrada ( <input asp-for="Movie.Title" class="form-control" /> ) usa los
atributos DataAnnotations y genera los atributos HTML necesarios para la validación de jQuery en el lado del
cliente.
En el siguiente tutorial se describe SQL Server LocalDB y la propagación de la base de datos.
A N T E R IO R : A D IC IÓ N D E U N S IG U IE N T E : S Q L S E R V E R
M ODELO LOCA LDB
Trabajar con SQL Server LocalDB y ASP.NET Core
14/05/2018 • 5 minutes to read • Edit Online

Por Rick Anderson y Joe Audette


El objeto MovieContext controla la tarea de conexión a la base de datos y de asignación de objetos Movie a los
registros de la base de datos. El contexto de base de datos se registra con el contenedor de inserción de
dependencias en el método ConfigureServices del archivo Startup.cs:

public void ConfigureServices(IServiceCollection services)


{
// requires
// using RazorPagesMovie.Models;
// using Microsoft.EntityFrameworkCore;

services.AddDbContext<MovieContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("MovieContext")));
services.AddMvc();
}

El sistema Configuración de ASP.NET Core lee el elemento ConnectionString . Para el desarrollo local, obtiene la
cadena de conexión del archivo appsettings.json:

"ConnectionStrings": {
"MovieContext": "Server=(localdb)\\mssqllocaldb;Database=Movie-
1;Trusted_Connection=True;MultipleActiveResultSets=true"
}

Al implementar la aplicación en un servidor de producción o de prueba, puede usar una variable de entorno u
otro enfoque para establecer la cadena de conexión en una instancia real de SQL Server. Para más información,
vea Configuración.

SQL Server Express LocalDB


LocalDB es una versión ligera del motor de base de datos de SQL Server Express dirigida al desarrollo de
programas. LocalDB se inicia a petición y se ejecuta en modo de usuario, sin necesidad de una configuración
compleja. De forma predeterminada, la base de datos LocalDB crea archivos "*.mdf" en el directorio
C:/Users/<usuario>.
En el menú Ver, abra Explorador de objetos de SQL Server (SSOX).
Haga clic con el botón derecho en la tabla Movie y seleccione Diseñador de vistas:

Observe el icono de llave junto a ID . De forma predeterminada, EF crea una propiedad denominada ID para la
clave principal.
Haga clic con el botón derecho en la tabla Movie y seleccione Ver datos:
Inicialización de la base de datos
Cree una nueva clase denominada SeedData en la carpeta Models. Reemplace el código generado con el
siguiente:
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Linq;

namespace RazorPagesMovie.Models
{
public static class SeedData
{
public static void Initialize(IServiceProvider serviceProvider)
{
using (var context = new MovieContext(
serviceProvider.GetRequiredService<DbContextOptions<MovieContext>>()))
{
// Look for any movies.
if (context.Movie.Any())
{
return; // DB has been seeded
}

context.Movie.AddRange(
new Movie
{
Title = "When Harry Met Sally",
ReleaseDate = DateTime.Parse("1989-2-12"),
Genre = "Romantic Comedy",
Price = 7.99M
},

new Movie
{
Title = "Ghostbusters",
ReleaseDate = DateTime.Parse("1984-3-13"),
Genre = "Comedy",
Price = 8.99M
},

new Movie
{
Title = "Ghostbusters 2",
ReleaseDate = DateTime.Parse("1986-2-23"),
Genre = "Comedy",
Price = 9.99M
},

new Movie
{
Title = "Rio Bravo",
ReleaseDate = DateTime.Parse("1959-4-15"),
Genre = "Western",
Price = 3.99M
}
);
context.SaveChanges();
}
}
}
}

Si hay alguna película en la base de datos, se devuelve el inicializador y no se agrega ninguna película.
if (context.Movie.Any())
{
return; // DB has been seeded.
}

Agregar el inicializador
Agregue el inicializador al final del método Main del archivo Program.cs:

// Unused usings removed.


using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using RazorPagesMovie.Models;
using System;
using Microsoft.EntityFrameworkCore;

namespace RazorPagesMovie
{
public class Program
{
public static void Main(string[] args)
{
var host = BuildWebHost(args);

using (var scope = host.Services.CreateScope())


{
var services = scope.ServiceProvider;

try
{
var context = services.GetRequiredService<MovieContext>();
// requires using Microsoft.EntityFrameworkCore;
context.Database.Migrate();
// Requires using RazorPagesMovie.Models;
SeedData.Initialize(services);
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred seeding the DB.");
}
}

host.Run();
}

public static IWebHost BuildWebHost(string[] args) =>


WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.Build();
}
}

Prueba de la aplicación
Elimine todos los registros de la base de datos. Puede hacerlo con los vínculos de eliminación en el
explorador o desde SSOX.
Obligue a la aplicación a inicializarse (llame a los métodos de la clase Startup ) para que se ejecute el
método de inicialización. Para forzar la inicialización, se debe detener y reiniciar IIS Express. Puede hacerlo
con cualquiera de los siguientes enfoques:
Haga clic con el botón derecho en el icono Bandeja del sistema de IIS Express del área de
notificación y pulse en Salir o en Detener sitio:

Si está ejecutando VS en modo de no depuración, presione F5 para ejecutar en modo de


depuración.
Si está ejecutando VS en modo de depuración, detenga el depurador y presione F5.
La aplicación muestra los datos inicializados:

El siguiente tutorial limpia la presentación de los datos.

A N T E R IO R : P Á G IN A S D E R A Z O R C O N S IG U IE N T E : A C T U A L IZ A C IÓ N D E L A S
S C A F F O L D IN G P Á G IN A S
Actualizar las páginas generadas en una aplicación
ASP.NET Core
14/05/2018 • 7 minutes to read • Edit Online

Por Rick Anderson


La aplicación de películas pinta bien, pero la presentación no es ideal. No queremos ver la hora (12:00:00 a.m. en
la imagen siguiente) y ReleaseDate debe ser Release Date (dos palabras).

Actualización del código generado


Abra el archivo Models/Movie.cs y agregue las líneas resaltadas mostradas en el código siguiente:
using System;

namespace RazorPagesMovie.Models
{
public class Movie
{
public int ID { get; set; }
public string Title { get; set; }

[Display(Name = "Release Date")]


[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }
public string Genre { get; set; }
public decimal Price { get; set; }
}
}

Haga clic con el botón derecho en una línea ondulada roja > ** Acciones rápidas y refactorizaciones**.

Seleccione using System.ComponentModel.DataAnnotations;

Visual Studio agrega using System.ComponentModel.DataAnnotations; .


En el próximo tutorial hablaremos de DataAnnotations. El atributo Display especifica qué se muestra como
nombre de un campo (en este caso, "Release Date" en lugar de "ReleaseDate"). El atributo DataType especifica el
tipo de los datos (Date), así que la información de hora almacenada en el campo no se muestra.
Vaya a Pages/Movies y mantenga el mouse sobre un vínculo de edición para ver la dirección URL de destino.
Los vínculos Edit, Details y Delete son generados por la aplicación auxiliar de etiquetas de delimitador del
archivo Pages/Movies/Index.cshtml.

@foreach (var item in Model.Movie) {


<tr>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.ReleaseDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Genre)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
<a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>

Las aplicaciones auxiliares de etiquetas permiten que el código de servidor participe en la creación y la
representación de elementos HTML en archivos de Razor. En el código anterior, AnchorTagHelper genera de forma
dinámica el valor del atributo href HTML desde la página de Razor (la ruta es relativa), el elemento asp-page y
el identificador de ruta ( asp-route-id ). Vea Generación de direcciones URL para las páginas para obtener más
información.
Use Ver código fuente en su explorador preferido para examinar el marcado generado. A continuación se
muestra una parte del HTML generado:
<td>
<a href="/Movies/Edit?id=1">Edit</a> |
<a href="/Movies/Details?id=1">Details</a> |
<a href="/Movies/Delete?id=1">Delete</a>
</td>

Los vínculos generados de forma dinámica pasan el identificador de la película con una cadena de consulta (por
ejemplo, http://localhost:5000/Movies/Details?id=2 ).
Actualice las páginas de edición, detalles y eliminación de Razor para usar la plantilla de ruta "{id:int}". Cambie la
directiva de página de cada una de estas páginas de @page a @page "{id:int}" . Ejecute la aplicación y luego vea
el origen. El HTML generado agrega el identificador a la parte de la ruta de acceso de la dirección URL:

<td>
<a href="/Movies/Edit/1">Edit</a> |
<a href="/Movies/Details/1">Details</a> |
<a href="/Movies/Delete/1">Delete</a>
</td>

Una solicitud a la página con la plantilla de ruta "{id:int}" que no incluya el entero devolverá un error HTTP 404
(no encontrado). Por ejemplo, http://localhost:5000/Movies/Details devolverá un error 404. Para que el
identificador sea opcional, anexe ? a la restricción de ruta:

@page "{id:int?}"

Actualización del control de excepciones de simultaneidad


Actualice el método OnPostAsync en el archivo Pages/Movies/Edit.cshtml.cs. En el código resaltado siguiente se
muestran los cambios:

public async Task<IActionResult> OnPostAsync()


{
if (!ModelState.IsValid)
{
return Page();
}

_context.Attach(Movie).State = EntityState.Modified;

try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!_context.Movie.Any(e => e.ID == Movie.ID))
{
return NotFound();
}
else
{
throw;
}
}

return RedirectToPage("./Index");
}

El código anterior solo detecta las excepciones de simultaneidad cuando el primer cliente simultáneo elimina la
película y el segundo cliente simultáneo publica cambios en ella.
Para probar el bloque catch :
Establecer un punto de interrupción en catch (DbUpdateConcurrencyException)
Edite una película.
En otra ventana del explorador, seleccione el vínculo de eliminación de la misma película y luego elimínela.
En la ventana anterior del explorador, publique los cambios en la película.
El código de producción por lo general debería detectar conflictos de simultaneidad cuando dos o más clientes
actualizan un registro de forma simultánea. Vea Administración de conflictos de simultaneidad para más
información.
Revisión de publicaciones y enlaces
Examine el archivo Pages/Movies/Edit.cshtml.cs: [!code-csharp]
Cuando se realiza una solicitud HTTP GET a la página Movies/Edit (por ejemplo,
http://localhost:5000/Movies/Edit/2 ):

El método OnGetAsync obtiene la película en la base de datos y devuelve el método Page .


El método Page presenta la página de Razor Pages/Movies/Edit.cshtml. El archivo Pages/Movies/Edit.cshtml
contiene la directiva de modelo ( @model RazorPagesMovie.Pages.Movies.EditModel ), que hace que el modelo de
película esté disponible en la página.
Se abre el formulario de edición con los valores de la película.
Cuando se publica la página Movies/Edit:
Los valores del formulario de la página se enlazan a la propiedad Movie . El atributo [BindProperty]
habilita el enlace de modelos.

[BindProperty]
public Movie Movie { get; set; }

Si hay errores en el estado del modelo (por ejemplo, ReleaseDate no se puede convertir en una fecha), el
formulario se vuelve a publicar con los valores enviados.
Si no hay ningún error en el modelo, se guarda la película.
Los métodos HTTP GET de las páginas de índice, creación y eliminación de Razor siguen un patrón similar. El
método HTTP POST OnPostAsync de la página de creación de Razor sigue un patrón similar al del método
OnPostAsync de la página de edición de Razor.

La búsqueda se incluye en el tutorial siguiente.

A N T E R IO R : T R A B A J A R C O N S Q L S E R V E R A D IC IÓ N D E
LOCA LDB BÚSQUEDA
Agregar búsqueda a páginas de Razor de ASP.NET
Core
25/06/2018 • 6 minutes to read • Edit Online

Por Rick Anderson


En este documento se agrega a la página de índice una capacidad de búsqueda que permite buscar películas por
género o nombre.
Actualice el método OnGetAsync de la página de índice con el código siguiente:

public async Task OnGetAsync(string searchString)


{
var movies = from m in _context.Movie
select m;

if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}

Movie = await movies.ToListAsync();


}

La primera línea del método OnGetAsync crea una consulta LINQ para seleccionar las películas:

var movies = from m in _context.Movie


select m;

En este momento solo se define la consulta, no se ejecuta en la base de datos.


Si el parámetro searchString contiene una cadena, la consulta de películas se modifica para filtrar según la
cadena de búsqueda:

if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}

El código s => s.Title.Contains() es una expresión lambda. Las lambdas se usan en consultas LINQ basadas en
métodos como argumentos para métodos de operador de consulta estándar, como el método Where o Contains
(usado en el código anterior). Las consultas LINQ no se ejecutan cuando se definen ni cuando se modifican
mediante una llamada a un método (como Where , Contains u OrderBy ). En su lugar, se aplaza la ejecución de la
consulta. Esto significa que la evaluación de una expresión se aplaza hasta que su valor realizado se repite o se
llama al método ToListAsync . Para más información, vea Query Execution (Ejecución de consultas).
Nota: El método Contains se ejecuta en la base de datos, no en el código de C#. La distinción entre mayúsculas y
minúsculas en la consulta depende de la base de datos y la intercalación. En SQL Server, Contains se asigna a
SQL LIKE, que distingue entre mayúsculas y minúsculas. En SQLite, con la intercalación predeterminada, se
distingue entre mayúsculas y minúsculas.
Vaya a la página de películas y anexe una cadena de consulta como ?searchString=Ghost a la dirección URL (por
ejemplo, http://localhost:5000/Movies?searchString=Ghost ). Se muestran las películas filtradas.

Si se agrega la siguiente plantilla de ruta a la página de índice, la cadena de búsqueda se puede pasar como un
segmento de dirección URL (por ejemplo, http://localhost:5000/Movies/Ghost ).

@page "{searchString?}"

La restricción de ruta anterior permite buscar el título como datos de ruta (un segmento de dirección URL ) en
lugar de como un valor de cadena de consulta. El elemento ? de "{searchString?}" significa que se trata de un
parámetro de ruta opcional.

Sin embargo, no se puede esperar que los usuarios modifiquen la dirección URL para buscar una película. En este
paso, se agrega la interfaz de usuario para filtrar las películas. Si ha agregado la restricción de ruta
"{searchString?}" , quítela.

Abra el archivo Pages/Movies/Index.cshtml y agregue el marcado <form> resaltado en el siguiente código:


@page
@model RazorPagesMovie.Pages.Movies.IndexModel

@{
ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
<a asp-page="Create">Create New</a>
</p>

<form>
<p>
Title: <input type="text" name="SearchString">
<input type="submit" value="Filter" />
</p>
</form>

<table class="table">
@*Markup removed for brevity.*@

La etiqueta HTML <form> usa la aplicación auxiliar de etiquetas de formulario. Cuando se envía el formulario, la
cadena de filtro se envía a la página Pages/Movies/Index. Guarde los cambios y pruebe el filtro.

Búsqueda por género


Agregue las siguientes propiedades resaltadas a Pages/Movies/Index.cshtml.cs:
[!code-csharp]
[!code-csharp]
El elemento SelectList Genres contiene la lista de géneros. Esto permite al usuario seleccionar un género de la
lista.
La propiedad MovieGenre contiene el género concreto que selecciona el usuario (por ejemplo, "Western").
Actualice el método OnGetAsync con el código siguiente:

// Requires using Microsoft.AspNetCore.Mvc.Rendering;


public async Task OnGetAsync(string movieGenre, string searchString)
{
// Use LINQ to get list of genres.
IQueryable<string> genreQuery = from m in _context.Movie
orderby m.Genre
select m.Genre;

var movies = from m in _context.Movie


select m;

if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}

if (!String.IsNullOrEmpty(movieGenre))
{
movies = movies.Where(x => x.Genre == movieGenre);
}
Genres = new SelectList(await genreQuery.Distinct().ToListAsync());
Movie = await movies.ToListAsync();
}

El código siguiente es una consulta LINQ que recupera todos los géneros de la base de datos.

// Use LINQ to get list of genres.


IQueryable<string> genreQuery = from m in _context.Movie
orderby m.Genre
select m.Genre;

La SelectList de géneros se crea mediante la proyección de los distintos géneros.

Genres = new SelectList(await genreQuery.Distinct().ToListAsync());

Adición de búsqueda por género


Actualice Index.cshtml como se indica a continuación:
@page
@model RazorPagesMovie.Pages.Movies.IndexModel

@{
ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
<a asp-page="Create">Create New</a>
</p>

<form>
<p>
<select asp-for="MovieGenre" asp-items="Model.Genres">
<option value="">All</option>
</select>

Title: <input type="text" name="SearchString">


<input type="submit" value="Filter" />
</p>
</form>

<table class="table">
<thead>

Pruebe la aplicación al buscar por género, título de la película y ambos.

A N T E R IO R : A C T U A L IZ A C IÓ N D E S IG U IE N T E : A D IC IÓ N D E U N N U E V O
P Á G IN A S CAM PO
Agregar un campo nuevo a una página de Razor en
ASP.NET Core
14/05/2018 • 7 minutes to read • Edit Online

Por Rick Anderson


En esta sección se usa Migraciones de Entity Framework Code First para agregar un nuevo campo al modelo y
migrar ese cambio a la base de datos.
Cuando se usa EF Code First para crear una base de datos de forma automática, Code First agrega una tabla a la
base de datos para ayudar a saber si el esquema de la base de datos está sincronizado con las clases del modelo
a partir del que se ha generado. Si no está sincronizado, EF produce una excepción. Esto facilita la detección de
problemas de código o base de datos incoherentes.

Adición de una propiedad de clasificación al modelo Movie


Abra el archivo Models/Movie.cs y agregue una propiedad Rating :

public class Movie


{
public int ID { get; set; }
public string Title { get; set; }

[Display(Name = "Release Date")]


[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }
public string Genre { get; set; }
public decimal Price { get; set; }
public string Rating { get; set; }
}

Compile la aplicación (Ctrl + Mayús + B ).


Edite Pages/Movies/Index.cshtml y agregue un campo Rating :

@page
@model RazorPagesMovie.Pages.Movies.IndexModel

@{
ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
<a asp-page="Create">Create New</a>
</p>

<form>
<p>
<select asp-for="MovieGenre" asp-items="Model.Genres">
<option value="">All</option>
</select>

Title: <input type="text" name="SearchString">


<input type="submit" value="Filter" />
</p>
</p>
</form>

<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Movie[0].Title)
</th>
<th>
@Html.DisplayNameFor(model => model.Movie[0].ReleaseDate)
</th>
<th>
@Html.DisplayNameFor(model => model.Movie[0].Genre)
</th>
<th>
@Html.DisplayNameFor(model => model.Movie[0].Price)
</th>
<th>
@Html.DisplayNameFor(model => model.Movie[0].Rating)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Movie) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.ReleaseDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Genre)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
@Html.DisplayFor(modelItem => item.Rating)
</td>
<td>
<a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>

Agregue el campo Rating a las páginas Delete y Details.


Actualice Create.cshtml con un campo Rating . Puede copiar o pegar el elemento <div> anterior y permitir que
IntelliSense le ayude a actualizar los campos. IntelliSense funciona con aplicaciones auxiliares de etiquetas.
El código siguiente muestra Create.cshtml con un campo Rating :
@page
@model RazorPagesMovie.Pages.Movies.CreateModel

@{
ViewData["Title"] = "Create";
}

<h2>Create</h2>

<h4>Movie</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Movie.Title" class="control-label"></label>
<input asp-for="Movie.Title" class="form-control" />
<span asp-validation-for="Movie.Title" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Movie.ReleaseDate" class="control-label"></label>
<input asp-for="Movie.ReleaseDate" class="form-control" />
<span asp-validation-for="Movie.ReleaseDate" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Movie.Genre" class="control-label"></label>
<input asp-for="Movie.Genre" class="form-control" />
<span asp-validation-for="Movie.Genre" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Movie.Price" class="control-label"></label>
<input asp-for="Movie.Price" class="form-control" />
<span asp-validation-for="Movie.Price" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Movie.Rating" class="control-label"></label>
<input asp-for="Movie.Rating" class="form-control" />
<span asp-validation-for="Movie.Rating" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</form>
</div>
</div>

<div>
<a asp-page="Index">Back to List</a>
</div>

@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

Agregue el campo Rating a la página de edición.


La aplicación no funciona hasta que la base de datos se actualiza para incluir el nuevo campo. Si se ejecuta ahora,
la aplicación produce una SqlException :

SqlException: Invalid column name 'Rating'.

Este error se debe a que la clase del modelo Movie actualizado es diferente al esquema de la tabla Movie de la
base de datos. (No hay ninguna columna Rating en la tabla de la base de datos).
Este error se puede resolver de varias maneras:
1. Haga que Entity Framework quite de forma automática la base de datos y la vuelva a crear con el nuevo
esquema de la clase del modelo. Este enfoque resulta conveniente al principio del ciclo de desarrollo;
permite desarrollar a la vez y de manera rápida el esquema del modelo y la base de datos. El
inconveniente es que se pierden los datos existentes en la base de datos. No use este enfoque en una base
de datos de producción. El quitar la base de datos en los cambios de esquema y el usar un inicializador
para inicializar automáticamente la base de datos con datos de prueba suele ser una forma productiva de
desarrollar una aplicación.
2. Modifique explícitamente el esquema de la base de datos existente para que coincida con las clases del
modelo. La ventaja de este enfoque es que se conservan los datos. Puede realizar este cambio de forma
manual o mediante la creación de un script de cambio de base de datos.
3. Use Migraciones de Code First para actualizar el esquema de la base de datos.
Para este tutorial, use Migraciones de Code First.
Actualice la clase SeedData para que proporcione un valor para la nueva columna. A continuación se muestra un
cambio de ejemplo, aunque es conveniente realizar este cambio para cada bloque new Movie .

context.Movie.AddRange(
new Movie
{
Title = "When Harry Met Sally",
ReleaseDate = DateTime.Parse("1989-2-12"),
Genre = "Romantic Comedy",
Price = 7.99M,
Rating = "R"
},

Vea el archivo completado SeedData.cs.


Compile la solución.
En el menú Herramientas, seleccione Administrador de paquetes NuGet > Consola del Administrador de
paquetes. En PCM, escriba los siguientes comandos:

Add-Migration Rating
Update-Database

El comando Add-Migration indica al marco de trabajo que:


Compare el modelo Movie con el esquema de base de datos Movie .
Cree código para migrar el esquema de la base de datos al nuevo modelo.
El nombre "Rating" es arbitrario y se usa para asignar nombre al archivo de migración. Resulta útil emplear un
nombre descriptivo para el archivo de migración.
Si elimina todos los registros de la base de datos, el inicializador inicializa la base de datos e incluye el campo
Rating . Puede hacerlo con los vínculos de eliminación en el explorador o desde el Explorador de objetos de SQL
Server (SSOX). Para eliminar la base de datos desde SSOX:
Seleccione la base de datos en SSOX.
Haga clic con el botón derecho en la base de datos y seleccione Eliminar.
Active Cerrar las conexiones existentes.
Seleccione Aceptar.
En la PMC, actualice la base de datos:

Update-Database

Ejecute la aplicación y compruebe que puede crear, editar o mostrar películas con un campo Rating . Si la base
de datos no está inicializada, detenga IIS Express y luego ejecute la aplicación.

A N T E R IO R : A D IC IÓ N D E S IG U IE N T E : A D IC IÓ N D E U N A
BÚSQUEDA V A L ID A C IÓ N
Agregar la validación a una página de Razor de
ASP.NET Core
17/05/2018 • 15 minutes to read • Edit Online

Por Rick Anderson


En esta sección se agrega lógica de validación al modelo Movie . Las reglas de validación se aplican cada vez que
un usuario crea o edita una película.

Validación
Un principio clave de desarrollo de software se denomina DRY, por "Don't Repeat Yourself" (Una vez y solo una).
Las páginas de Razor fomentan un tipo de desarrollo en el que la funcionalidad se especifica una vez y se refleja
en la aplicación. DRY puede ayudar a reducir la cantidad de código en una aplicación. DRY hace que el código sea
menos propenso a errores y resulte más fácil de probar y mantener.
La compatibilidad de validación proporcionada por las páginas de Razor y Entity Framework es un buen ejemplo
del principio DRY. Las reglas de validación se especifican mediante declaración en un solo lugar (en la clase del
modelo) y se aplican en toda la aplicación.
Adición de reglas de validación al modelo de película
Abra el archivo Movie.cs. DataAnnotations proporciona un conjunto integrado de atributos de validación que se
aplican mediante declaración a una clase o propiedad. DataAnnotations también contiene atributos de formato
como DataType que ayudan a aplicar formato y no proporcionan validación.
Actualice la clase Movie para aprovechar los atributos de validación Required , StringLength , RegularExpression
y Range .

public class Movie


{
public int ID { get; set; }

[StringLength(60, MinimumLength = 3)]


[Required]
public string Title { get; set; }

[Display(Name = "Release Date")]


[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }

[Range(1, 100)]
[DataType(DataType.Currency)]
public decimal Price { get; set; }

[RegularExpression(@"^[A-Z]+[a-zA-Z""'\s-]*$")]
[Required]
[StringLength(30)]
public string Genre { get; set; }

[RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$")]
[StringLength(5)]
[Required]
public string Rating { get; set; }
}
Los atributos de validación especifican el comportamiento que se aplica a las propiedades del modelo:
Los atributos Required y MinimumLength indican que una propiedad debe tener un valor. Aun así, nada impide
que el usuario escriba un espacio en blanco para cumplir la restricción de validación de un tipo que acepta
valores NULL. Los tipos de valor que no aceptan valores NULL (como decimal , int , float y DateTime ) son
intrínsecamente necesarios y no necesitan el atributo Required .
El atributo RegularExpression limita los caracteres que el usuario puede escribir. En el código anterior, Genre y
Rating solo deben usar letras (no se permiten espacios en blanco, números ni caracteres especiales).
El atributo Range restringe un valor a un intervalo determinado.
El atributo StringLength establece la longitud máxima de una cadena y, de forma opcional, la longitud mínima.
El que ASP.NET Core aplique automáticamente las reglas de validación ayuda a que una aplicación sea más
sólida. La validación automática en modelos ayuda a proteger la aplicación, ya que no hay que acordarse de su
aplicación cuando se agrega nuevo código.
Interfaz de usuario de error de validación en páginas de Razor
Ejecute la aplicación y vaya a Pages/Movies.
Seleccione el vínculo Crear nuevo. Rellene el formulario con algunos valores no válidos. Cuando la validación de
cliente de jQuery detecta el error, muestra un mensaje de error.
NOTE
Es posible que no pueda escribir comas ni puntos decimales en el campo Price . Para admitir la validación de jQuery en
configuraciones regionales distintas del inglés que usan una coma (",") en lugar de un punto decimal y formatos de fecha
distintos del inglés (EE. UU.), debe tomar medidas para globalizar la aplicación. Para más información, vea Recursos
adicionales. Por ahora, escriba solamente números enteros como 10.

Observe cómo el formulario presenta automáticamente un mensaje de error de validación en cada campo que
contiene un valor no válido. Los errores se aplican al cliente (con JavaScript y jQuery) y al servidor (cuando un
usuario tiene JavaScript deshabilitado).
Una ventaja importante es que no se han necesitado cambios de código en las páginas de creación o edición. Una
vez que DataAnnotations se ha aplicado al modelo, la interfaz de usuario de validación se ha habilitado. Las
páginas de Razor creadas en este tutorial han obtenido automáticamente las reglas de validación (mediante
atributos de validación en las propiedades de la clase del modelo Movie ). Al probar la validación en la página de
edición, se aplica la misma validación.
Los datos del formulario no se publicarán en el servidor hasta que dejen de producirse errores de validación de
cliente. Compruebe que los datos del formulario no se publican mediante uno o varios de los métodos siguientes:
Coloque un punto de interrupción en el método OnPostAsync . Envíe el formulario (seleccione Crear o
Guardar). El punto de interrupción nunca se alcanza.
Use la herramienta Fiddler.
Use las herramientas de desarrollo del explorador para supervisar el tráfico de red.
Validación de servidor
Cuando JavaScript está deshabilitado en el explorador, si se envía el formulario con errores, se publica en el
servidor.
Validación de servidor de prueba opcional:
Deshabilite JavaScript en el explorador. Si no se puede deshabilitar JavaScript en el explorador, pruebe con
otro explorador.
Establezca un punto de interrupción en el método OnPostAsync de la página de creación o edición.
Envíe un formulario con errores de validación.
Compruebe que el estado del modelo no es válido:

if (!ModelState.IsValid)
{
return Page();
}

El código siguiente muestra una parte de la página Create.cshtml a la que se ha aplicado scaffolding
anteriormente en el tutorial. Las páginas de creación y edición la usan para mostrar el formulario inicial y para
volver a mostrar el formulario en caso de error.

<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Movie.Title" class="control-label"></label>
<input asp-for="Movie.Title" class="form-control" />
<span asp-validation-for="Movie.Title" class="text-danger"></span>
</div>

La aplicación auxiliar de etiquetas de entrada usa los atributos DataAnnotations y genera los atributos HTML
necesarios para la validación de jQuery en el cliente. La aplicación auxiliar de etiquetas de validación muestra
errores de validación. Para más información, vea Validación.
Las páginas de creación y edición no tienen ninguna regla de validación. Las reglas de validación y las cadenas de
error solo se especifican en la clase Movie . Estas reglas de validación se aplican automáticamente a las páginas de
Razor que editan el modelo Movie .
Cuando es necesario modificar la lógica de validación, se hace únicamente en el modelo. La validación se aplica de
forma coherente en toda la aplicación (la lógica de validación se define en un solo lugar). La validación en un solo
lugar ayuda a mantener limpio el código y facilita su mantenimiento y actualización.

Uso de atributos DataType


Examine la clase Movie . El espacio de nombres System.ComponentModel.DataAnnotations proporciona atributos de
formato además del conjunto integrado de atributos de validación. El atributo DataType se aplica a las
propiedades ReleaseDate y Price .

[Display(Name = "Release Date")]


[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }

[Range(1, 100)]
[DataType(DataType.Currency)]
public decimal Price { get; set; }

Los atributos DataType solo proporcionan sugerencias para que el motor de vista aplique formato a los datos (y
ofrece atributos como <a> para las direcciones URL y <a href="mailto:EmailAddress.com"> para el correo
electrónico). Use el atributo RegularExpression para validar el formato de los datos. El atributo DataType se usa
para especificar un tipo de datos más específico que el tipo intrínseco de base de datos. Los atributos DataType
no son atributos de validación. En la aplicación de ejemplo solo se muestra la fecha, sin hora.
La enumeración DataType proporciona muchos tipos de datos, como Date, Time, PhoneNumber, Currency,
EmailAddress, etc. El atributo DataType también puede permitir que la aplicación proporcione automáticamente
características específicas del tipo. Por ejemplo, se puede crear un vínculo mailto: para DataType.EmailAddress .
Se puede proporcionar un selector de fecha para DataType.Date en exploradores compatibles con HTML5. Los
atributos DataType emiten atributos HTML 5 data- (se pronuncia "datos dash") para su uso por parte de los
exploradores HTML 5. Los atributos DataType no proporcionan ninguna validación.
DataType.Date no especifica el formato de la fecha que se muestra. De manera predeterminada, el campo de
datos se muestra según los formatos predeterminados basados en el elemento CultureInfo del servidor.
El atributo DisplayFormat se usa para especificar el formato de fecha de forma explícita:

[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]


public DateTime ReleaseDate { get; set; }

El valor ApplyFormatInEditMode especifica que el formato se debe aplicar cuando el valor se muestra para su
edición. Es posible que no quiera ese comportamiento para algunos campos. Por ejemplo, en los valores de
moneda, probablemente no quiera el símbolo de moneda en la interfaz de usuario de edición.
El atributo DisplayFormat puede usarse por sí mismo, pero normalmente es buena idea usar el atributo DataType
. El atributo DataType transmite la semántica de los datos en contraposición a cómo se representan en una
pantalla y ofrece las siguientes ventajas que no proporciona DisplayFormat:
El explorador puede habilitar características de HTML5 (por ejemplo, mostrar un control de calendario, el
símbolo de moneda adecuado según la configuración regional, vínculos de correo electrónico, etc.).
De forma predeterminada, el explorador presenta los datos con el formato correcto en función de la
configuración regional.
El atributo DataType puede habilitar el marco de trabajo de ASP.NET Core para elegir la plantilla de campo
correcta a fin de presentar los datos. DisplayFormat , si se emplea por sí mismo, usa la plantilla de cadena.

Nota: La validación de jQuery no funciona con el atributo Range ni DateTime . Por ejemplo, el código siguiente
siempre muestra un error de validación de cliente, incluso cuando la fecha está en el intervalo especificado:

[Range(typeof(DateTime), "1/1/1966", "1/1/2020")]

Por lo general no se recomienda compilar fechas fijas en los modelos, así que se desaconseja el empleo del
atributo Range y DateTime .
El código siguiente muestra la combinación de atributos en una línea:

public class Movie


{
public int ID { get; set; }

[StringLength(60, MinimumLength = 3)]


public string Title { get; set; }

[Display(Name = "Release Date"), DataType(DataType.Date)]


public DateTime ReleaseDate { get; set; }

[RegularExpression(@"^[A-Z]+[a-zA-Z""'\s-]*$"), Required, StringLength(30)]


public string Genre { get; set; }

[Range(1, 100), DataType(DataType.Currency)]


public decimal Price { get; set; }

[RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$"), StringLength(5)]
public string Rating { get; set; }
}

En Introducción a las páginas de Razor y EF Core se muestran operaciones más avanzadas de EF Core con
páginas de Razor.
Publicar en Azure
Vea Publicar una aplicación web de ASP.NET Core en Azure App Service con Visual Studio para obtener
instrucciones sobre cómo publicar esta aplicación en Azure.

Recursos adicionales
Trabajar con formularios
Globalización y localización
Introducción a las aplicaciones auxiliares de etiquetas
Creación de aplicaciones auxiliares de etiquetas

A N T E R IO R : A D IC IÓ N D E U N N U E V O S IG U IE N T E : C A R G A D E
CAM PO A R C H IV O S
Cargar archivos en una página de Razor en ASP.NET
Core
25/06/2018 • 21 minutes to read • Edit Online

Por Luke Latham


En esta sección se muestra cómo cargar archivos con una página de Razor.
La aplicación de ejemplo Razor Pages Movie de este tutorial usa el enlace de modelo simple para cargar archivos,
lo que funciona bien para cargar archivos pequeños. Para más información sobre la transmisión de archivos de
gran tamaño, vea Uploading large files with streaming (Carga de archivos grandes mediante transmisión).
En los pasos siguientes se agrega a la aplicación de ejemplo una característica de carga de archivo de
programación de película. Una programación de película está representada por una clase Schedule . La clase
incluye dos versiones de la programación. Una versión se proporciona a los clientes, PublicSchedule . La otra se
usa para los empleados de la empresa, PrivateSchedule . Cada versión se carga como un archivo independiente. El
tutorial muestra cómo realizar dos cargas de archivos desde una página con un solo elemento POST en el
servidor.

Consideraciones de seguridad
Debe tener precaución al proporcionar a los usuarios la capacidad de cargar archivos en un servidor, ya que los
atacantes podrían ejecutar un ataque por denegación de servicio u otros ataques en un sistema. A continuación se
muestran algunos pasos de seguridad con los que se reduce la probabilidad de sufrir ataques:
Cargue archivos en un área de carga de archivos específica del sistema, con lo que resulta más fácil imponer
medidas de seguridad en el contenido cargado. Al permitir las cargas de archivos, asegúrese de que los
permisos de ejecución están deshabilitados en la ubicación de carga.
Use un nombre de archivo seguro determinado por la aplicación, y no por la entrada del usuario o el nombre
de archivo del archivo cargado.
Permita únicamente un conjunto específico de extensiones de archivo aprobadas.
Compruebe que se llevan a cabo comprobaciones de cliente en el servidor. Las comprobaciones de cliente son
fáciles de sortear.
Compruebe el tamaño de la carga y evite las cargas de mayor tamaño de lo esperado.
Ejecute un escáner de virus/malware en el contenido cargado.

WARNING
La carga de código malintencionado en un sistema suele ser el primer paso para ejecutar código que puede:
Tomar todo el poder en un sistema.
Sobrecargar un sistema de manera que se producen errores generales del sistema.
Poner en peligro los datos del usuario o del sistema.
Aplicar grafitis a una interfaz pública.

Adición de una clase FileUpload


Cree una página de Razor para controlar un par de cargas de archivos. Agregue una clase FileUpload , que está
enlazada a la página para obtener los datos de programación. Haga clic con el botón derecho en la carpeta Models.
Seleccione Agregar > Clase. Asigne a la clase el nombre FileUpload y agregue las siguientes propiedades:

using Microsoft.AspNetCore.Http;
using System.ComponentModel.DataAnnotations;

namespace RazorPagesMovie.Models
{
public class FileUpload
{
[Required]
[Display(Name="Title")]
[StringLength(60, MinimumLength = 3)]
public string Title { get; set; }

[Required]
[Display(Name="Public Schedule")]
public IFormFile UploadPublicSchedule { get; set; }

[Required]
[Display(Name="Private Schedule")]
public IFormFile UploadPrivateSchedule { get; set; }
}
}

La clase tiene una propiedad para el título de la programación y otra para cada una de las dos versiones de la
programación. Las tres propiedades son necesarias y el título debe tener entre 3 y 60 caracteres.

Agregar un método auxiliar para cargar archivos


Para evitar la duplicación de código para el procesamiento de archivos de programación cargados, primero
agregue un método auxiliar estático. Cree una carpeta Utilities en la aplicación y agregue un archivo FileHelpers.cs
con el siguiente contenido. El método auxiliar, ProcessFormFile , toma un elemento IFormFile y
ModelStateDictionary y devuelve una cadena con el contenido y el tamaño del archivo. Se comprueban el tipo de
contenido y la longitud. Si el archivo no pasa una comprobación de validación, se agrega un error a ModelState .

using System;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Net;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using RazorPagesMovie.Models;

namespace RazorPagesMovie.Utilities
{
public class FileHelpers
{
public static async Task<string> ProcessFormFile(IFormFile formFile, ModelStateDictionary modelState)
{
var fieldDisplayName = string.Empty;

// Use reflection to obtain the display name for the model


// property associated with this IFormFile. If a display
// name isn't found, error messages simply won't show
// a display name.
MemberInfo property =
typeof(FileUpload).GetProperty(formFile.Name.Substring(formFile.Name.IndexOf(".") + 1));

if (property != null)
{
var displayAttribute =
var displayAttribute =
property.GetCustomAttribute(typeof(DisplayAttribute)) as DisplayAttribute;

if (displayAttribute != null)
{
fieldDisplayName = $"{displayAttribute.Name} ";
}
}

// Use Path.GetFileName to obtain the file name, which will


// strip any path information passed as part of the
// FileName property. HtmlEncode the result in case it must
// be returned in an error message.
var fileName = WebUtility.HtmlEncode(Path.GetFileName(formFile.FileName));

if (formFile.ContentType.ToLower() != "text/plain")
{
modelState.AddModelError(formFile.Name,
$"The {fieldDisplayName}file ({fileName}) must be a text file.");
}

// Check the file length and don't bother attempting to


// read it if the file contains no content. This check
// doesn't catch files that only have a BOM as their
// content, so a content length check is made later after
// reading the file's content to catch a file that only
// contains a BOM.
if (formFile.Length == 0)
{
modelState.AddModelError(formFile.Name, $"The {fieldDisplayName}file ({fileName}) is empty.");
}
else if (formFile.Length > 1048576)
{
modelState.AddModelError(formFile.Name, $"The {fieldDisplayName}file ({fileName}) exceeds 1
MB.");
}
else
{
try
{
string fileContents;

// The StreamReader is created to read files that are UTF-8 encoded.


// If uploads require some other encoding, provide the encoding in the
// using statement. To change to 32-bit encoding, change
// new UTF8Encoding(...) to new UTF32Encoding().
using (
var reader =
new StreamReader(
formFile.OpenReadStream(),
new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes:
true),
detectEncodingFromByteOrderMarks: true))
{
fileContents = await reader.ReadToEndAsync();

// Check the content length in case the file's only


// content was a BOM and the content is actually
// empty after removing the BOM.
if (fileContents.Length > 0)
{
return fileContents;
}
else
{
modelState.AddModelError(formFile.Name,
$"The {fieldDisplayName}file ({fileName}) is empty.");
}
}
}
}
catch (Exception ex)
{
modelState.AddModelError(formFile.Name,
$"The {fieldDisplayName}file ({fileName}) upload failed. " +
$"Please contact the Help Desk for support. Error:
{ex.Message}");
// Log the exception
}
}

return string.Empty;
}
}
}

Guardar el archivo en el disco


La aplicación de ejemplo guarda los archivos cargados en campos de base de datos. Para guardar un archivo en
disco, use una clase FileStream. En el siguiente ejemplo, un archivo contenido en FileUpload.UploadPublicSchedule
se copia en una clase FileStream de un método OnPostAsync . FileStream escribe el archivo en disco en el
<PATH-AND-FILE-NAME> proporcionado:

public async Task<IActionResult> OnPostAsync()


{
// Perform an initial check to catch FileUpload class attribute violations.
if (!ModelState.IsValid)
{
return Page();
}

var filePath = "<PATH-AND-FILE-NAME>";

using (var fileStream = new FileStream(filePath, FileMode.Create))


{
await FileUpload.UploadPublicSchedule.CopyToAsync(fileStream);
}

return RedirectToPage("./Index");
}

El proceso de trabajo debe tener permisos de escritura en la ubicación especificada por filePath .

NOTE
filePath debe incluir el nombre de archivo. Si no se ha proporcionado un nombre de archivo, se produce una excepción
UnauthorizedAccessException en tiempo de ejecución.

WARNING
Los archivos cargados nunca deben persistir en el mismo árbol de directorio que la aplicación.
En el código de ejemplo no se proporciona ningún tipo de protección de servidor frente a cargas de archivos
malintencionados. Vea los siguientes recursos para más información sobre cómo reducir el área expuesta de ataques al
aceptar archivos de los usuarios:
Unrestricted File Upload (Carga de archivos sin restricciones)
Azure Security: asegúrese de que los controles adecuados estén en vigor al aceptar archivos de usuarios

Guardar el archivo en Azure Blob Storage


Para cargar el contenido del archivo en Azure Blob Storage, vea Introducción a Azure Blob Storage mediante
.NET. En el tema se muestra cómo usar UploadFromStream para guardar una clase FileStream en Blob Storage.

Adición de la clase Schedule


Haga clic con el botón derecho en la carpeta Models. Seleccione Agregar > Clase. Asigne a la clase el nombre
Schedule y agregue las siguientes propiedades:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace RazorPagesMovie.Models
{
public class Schedule
{
public int ID { get; set; }
public string Title { get; set; }

public string PublicSchedule { get; set; }

[Display(Name = "Public Schedule Size (bytes)")]


[DisplayFormat(DataFormatString = "{0:N1}")]
public long PublicScheduleSize { get; set; }

public string PrivateSchedule { get; set; }

[Display(Name = "Private Schedule Size (bytes)")]


[DisplayFormat(DataFormatString = "{0:N1}")]
public long PrivateScheduleSize { get; set; }

[Display(Name = "Uploaded (UTC)")]


[DisplayFormat(DataFormatString = "{0:F}")]
public DateTime UploadDT { get; set; }
}
}

La clase usa los atributos Display y DisplayFormat , que generan títulos descriptivos y formato cuando se
representan los datos de programación.

Actualización de MovieContext
Especifique DbSet en MovieContext (Models/MovieContext.cs) para las programaciones:

using Microsoft.EntityFrameworkCore;

namespace RazorPagesMovie.Models
{
public class MovieContext : DbContext
{
public MovieContext(DbContextOptions<MovieContext> options)
: base(options)
{
}

public DbSet<Movie> Movie { get; set; }


public DbSet<Schedule> Schedule { get; set; }
}
}
Adición de la tabla Schedule a la base de datos
Abra la Consola del Administrador de paquetes (PMC ): Herramientas > Administrador de paquetes NuGet >
Consola del Administrador de paquetes.

En la PMC, ejecute los siguientes comandos. Estos comandos agregan una tabla Schedule a la base de datos:

Add-Migration AddScheduleTable
Update-Database

Adición de una página de Razor de carga de archivos


En la carpeta Pages, cree una carpeta Schedules. En la carpeta Schedules, cree una página denominada
Index.cshtml para cargar una programación con el siguiente contenido:

@page
@model RazorPagesMovie.Pages.Schedules.IndexModel

@{
ViewData["Title"] = "Schedules";
}

<h2>Schedules</h2>
<hr />

<h3>Upload Schedules</h3>
<div class="row">
<div class="col-md-4">
<form method="post" enctype="multipart/form-data">
<div class="form-group">
<label asp-for="FileUpload.Title" class="control-label"></label>
<input asp-for="FileUpload.Title" type="text" class="form-control" />
<span asp-validation-for="FileUpload.Title" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="FileUpload.UploadPublicSchedule" class="control-label"></label>
<input asp-for="FileUpload.UploadPublicSchedule" type="file" class="form-control"
style="height:auto" />
<span asp-validation-for="FileUpload.UploadPublicSchedule" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="FileUpload.UploadPrivateSchedule" class="control-label"></label>
<input asp-for="FileUpload.UploadPrivateSchedule" type="file" class="form-control"
style="height:auto" />
<span asp-validation-for="FileUpload.UploadPrivateSchedule" class="text-danger"></span>
</div>
<input type="submit" value="Upload" class="btn btn-default" />
</form>
</div>
</div>

<h3>Loaded Schedules</h3>
<table class="table">
<thead>
<tr>
<th></th>
<th>
@Html.DisplayNameFor(model => model.Schedule[0].Title)
</th>
<th>
@Html.DisplayNameFor(model => model.Schedule[0].UploadDT)
</th>
<th class="text-center">
@Html.DisplayNameFor(model => model.Schedule[0].PublicScheduleSize)
</th>
<th class="text-center">
@Html.DisplayNameFor(model => model.Schedule[0].PrivateScheduleSize)
</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Schedule) {
<tr>
<td>
<a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
</td>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.UploadDT)
</td>
<td class="text-center">
@Html.DisplayFor(modelItem => item.PublicScheduleSize)
</td>
<td class="text-center">
@Html.DisplayFor(modelItem => item.PrivateScheduleSize)
</td>
</tr>
}
</tbody>
</table>

@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

Cada grupo del formulario incluye una <etiqueta> que muestra el nombre de cada propiedad de clase. Los
atributos Display del modelo FileUpload proporcionan los valores de presentación de las etiquetas. Por ejemplo,
el nombre para mostrar de la propiedad UploadPublicSchedule se establece con
[Display(Name="Public Schedule")] y, por tanto, muestra "Programación pública" en la etiqueta cuando se
presenta el formulario.
Cada grupo del formulario incluye un <intervalo> de validación. Si la entrada del usuario no cumple los
atributos de propiedad establecidos en la clase FileUpload o si se produce un error en alguna de las
comprobaciones de validación del archivo del método ProcessFormFile , no se valida el modelo. Cuando se
produce un error en la validación del modelo, se presenta un útil mensaje de validación al usuario. Por ejemplo, la
propiedad Title se anota con [Required] y [StringLength(60, MinimumLength = 3)] . Si el usuario no proporciona
un título, recibe un mensaje que indica que se necesita un valor. Si el usuario especifica un valor de menos de tres
caracteres o de más de sesenta caracteres, recibe un mensaje que indica que el valor tiene una longitud incorrecta.
Si se proporciona un archivo sin contenido, aparece un mensaje que indica que el archivo está vacío.

Agregar el modelo de página


Agregue el modelo de página (Index.cshtml.cs) a la carpeta Schedules:

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Models;
using RazorPagesMovie.Utilities;

namespace RazorPagesMovie.Pages.Schedules
{
public class IndexModel : PageModel
{
private readonly RazorPagesMovie.Models.MovieContext _context;

public IndexModel(RazorPagesMovie.Models.MovieContext context)


{
_context = context;
}

[BindProperty]
public FileUpload FileUpload { get; set; }

public IList<Schedule> Schedule { get; private set; }

public async Task OnGetAsync()


{
Schedule = await _context.Schedule.AsNoTracking().ToListAsync();
}

public async Task<IActionResult> OnPostAsync()


{
// Perform an initial check to catch FileUpload class
// attribute violations.
if (!ModelState.IsValid)
{
Schedule = await _context.Schedule.AsNoTracking().ToListAsync();
return Page();
}

var publicScheduleData =
await FileHelpers.ProcessFormFile(FileUpload.UploadPublicSchedule, ModelState);

var privateScheduleData =
await FileHelpers.ProcessFormFile(FileUpload.UploadPrivateSchedule, ModelState);

// Perform a second check to catch ProcessFormFile method


// violations.
if (!ModelState.IsValid)
{
Schedule = await _context.Schedule.AsNoTracking().ToListAsync();
return Page();
}

var schedule = new Schedule()


{
PublicSchedule = publicScheduleData,
PublicSchedule = publicScheduleData,
PublicScheduleSize = FileUpload.UploadPublicSchedule.Length,
PrivateSchedule = privateScheduleData,
PrivateScheduleSize = FileUpload.UploadPrivateSchedule.Length,
Title = FileUpload.Title,
UploadDT = DateTime.UtcNow
};

_context.Schedule.Add(schedule);
await _context.SaveChangesAsync();

return RedirectToPage("./Index");
}
}
}

El modelo de página ( IndexModel en Index.cshtml.cs) enlaza la clase FileUpload :

[BindProperty]
public FileUpload FileUpload { get; set; }

El modelo además usa una lista de las programaciones ( IList<Schedule> ) para mostrar las programaciones
almacenadas en la base de datos en la página:

public IList<Schedule> Schedule { get; private set; }

Cuando se carga la página con OnGetAsync , Schedules se rellena a partir de la base de datos y se usa para
generar una tabla HTML de programaciones cargadas:

public async Task OnGetAsync()


{
Schedule = await _context.Schedule.AsNoTracking().ToListAsync();
}

Cuando el formulario se publica en el servidor, se activa ModelState . Si no es válido, Schedule se vuelve a


generar y la página se presenta con uno o más mensajes de validación que indican el motivo del error de
validación de la página. Si es válido, las propiedades FileUpload se usan en OnPostAsync para completar la carga
de archivos para las dos versiones de la programación y para crear un nuevo objeto Schedule para almacenar los
datos. La programación luego se guarda en la base de datos:
public async Task<IActionResult> OnPostAsync()
{
// Perform an initial check to catch FileUpload class
// attribute violations.
if (!ModelState.IsValid)
{
Schedule = await _context.Schedule.AsNoTracking().ToListAsync();
return Page();
}

var publicScheduleData =
await FileHelpers.ProcessSchedule(FileUpload.UploadPublicSchedule, ModelState);

var privateScheduleData =
await FileHelpers.ProcessSchedule(FileUpload.UploadPrivateSchedule, ModelState);

// Perform a second check to catch ProcessSchedule method


// violations.
if (!ModelState.IsValid)
{
Schedule = await _context.Schedule.AsNoTracking().ToListAsync();
return Page();
}

var schedule = new Schedule()


{
PublicSchedule = publicScheduleData,
PublicScheduleSize = FileUpload.UploadPublicSchedule.Length,
PrivateSchedule = privateScheduleData,
PrivateScheduleSize = FileUpload.UploadPrivateSchedule.Length,
Title = FileUpload.Title,
UploadDT = DateTime.UtcNow
};

_context.Schedule.Add(schedule);
await _context.SaveChangesAsync();

return RedirectToPage("./Index");
}

Vinculación de una página de Razor de carga de archivos


Abra _Layout.cshtml y agregue un vínculo a la barra de navegación para llegar a la página de carga de archivos:

<div class="navbar-collapse collapse">


<ul class="nav navbar-nav">
<li><a asp-page="/Index">Home</a></li>
<li><a asp-page="/Schedules/Index">Schedules</a></li>
<li><a asp-page="/About">About</a></li>
<li><a asp-page="/Contact">Contact</a></li>
</ul>
</div>

Adición de una página para confirmar la eliminación de la


programación
Cuando el usuario hace clic para eliminar una programación, se le da la oportunidad de cancelar la operación.
Agregue una página de confirmación de eliminación (Delete.cshtml) a la carpeta Schedules:
@page "{id:int}"
@model RazorPagesMovie.Pages.Schedules.DeleteModel

@{
ViewData["Title"] = "Delete Schedule";
}

<h2>Delete Schedule</h2>

<h3>Are you sure you want to delete this?</h3>


<div>
<h4>Schedule</h4>
<hr />
<dl class="dl-horizontal">
<dt>
@Html.DisplayNameFor(model => model.Schedule.Title)
</dt>
<dd>
@Html.DisplayFor(model => model.Schedule.Title)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Schedule.PublicScheduleSize)
</dt>
<dd>
@Html.DisplayFor(model => model.Schedule.PublicScheduleSize)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Schedule.PrivateScheduleSize)
</dt>
<dd>
@Html.DisplayFor(model => model.Schedule.PrivateScheduleSize)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Schedule.UploadDT)
</dt>
<dd>
@Html.DisplayFor(model => model.Schedule.UploadDT)
</dd>
</dl>

<form method="post">
<input type="hidden" asp-for="Schedule.ID" />
<input type="submit" value="Delete" class="btn btn-default" /> |
<a asp-page="./Index">Back to List</a>
</form>
</div>

El modelo de página (Delete.cshtml.cs) carga una sola programación identificada por id en los datos de ruta de la
solicitud. Agregue el archivo Delete.cshtml.cs a la carpeta Schedules:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Models;

namespace RazorPagesMovie.Pages.Schedules
{
public class DeleteModel : PageModel
{
private readonly RazorPagesMovie.Models.MovieContext _context;

public DeleteModel(RazorPagesMovie.Models.MovieContext context)


{
_context = context;
}

[BindProperty]
public Schedule Schedule { get; set; }

public async Task<IActionResult> OnGetAsync(int? id)


{
if (id == null)
{
return NotFound();
}

Schedule = await _context.Schedule.SingleOrDefaultAsync(m => m.ID == id);

if (Schedule == null)
{
return NotFound();
}
return Page();
}

public async Task<IActionResult> OnPostAsync(int? id)


{
if (id == null)
{
return NotFound();
}

Schedule = await _context.Schedule.FindAsync(id);

if (Schedule != null)
{
_context.Schedule.Remove(Schedule);
await _context.SaveChangesAsync();
}

return RedirectToPage("./Index");
}
}
}

El método OnPostAsync controla la eliminación de la programación mediante su id :


public async Task<IActionResult> OnPostAsync(int? id)
{
if (id == null)
{
return NotFound();
}

Schedule = await _context.Schedule.FindAsync(id);

if (Schedule != null)
{
_context.Schedule.Remove(Schedule);
await _context.SaveChangesAsync();
}

return RedirectToPage("./Index");
}

Después de eliminar correctamente la programación, RedirectToPage vuelve a enviar al usuario a la página de


programaciones Index.cshtml.

Página de Razor Schedules activa


Cuando se carga la página, las etiquetas y las entradas del título de la programación, la programación pública y la
privada se presentan con un botón de envío:

La selección del botón Cargar sin rellenar ninguno de los campos infringe los atributos [Required] en el modelo.
ModelState no es válido. Los mensajes de error de validación se muestran al usuario:
Escriba dos letras en el campo Título. El mensaje de validación cambia para indicar que el título debe tener entre
3 y 60 caracteres:

Cuando se cargan una o más programaciones, la sección Programaciones cargadas presenta las
programaciones cargadas:

El usuario puede hacer clic en el vínculo Eliminar desde allí para llegar a la vista de confirmación de eliminación,
donde tiene una oportunidad de confirmar o cancelar la operación de eliminación.

Solución de problemas
Para más información de solución de problemas de carga de IFormFile , vea Cargas de archivos en ASP.NET
Core: Solución de problemas.
Gracias por seguir esta introducción a las páginas de Razor. Le agradeceremos cualquier comentario. Introducción
a MVC y EF Core es un excelente artículo de seguimiento de este tutorial.

Recursos adicionales
Cargas de archivos en ASP.NET Core
IFormFile

A N T E R IO R :
V A L ID A C IÓ N
Crear una aplicación web con ASP.NET Core MVC en
Windows con Visual Studio
11/04/2018 • 2 minutes to read • Edit Online

En este tutorial se muestra el desarrollo web de ASP.NET Core MVC con controladores y vistas. Las páginas de
Razor son una nueva alternativa en ASP.NET Core 2.0 y posteriores, un modelo de programación basado en
páginas que facilita la compilación de interfaces de usuario web y hace que sean más productivas. Se recomienda
probar el tutorial de las páginas de Razor antes que la versión MVC. El tutorial de las páginas de Razor:
Es el método preferido para el desarrollo de nuevas aplicaciones.
Es más fácil de seguir.
Abarca más características.
Si elige este tutorial en lugar de la versión de páginas de Razor, le agradeceremos que nos explique el motivo en
este problema de GitHub.
Hay tres versiones de este tutorial:
Windows: Esta serie
macOS: Creación de una aplicación de ASP.NET Core MVC con Visual Studio para Mac
macOS, Linux y Windows: Crear una aplicación de ASP.NET Core MVC con Visual Studio Code La serie de
tutoriales incluye lo siguiente:
1. Introducción
2. Agregar un controlador
3. Agregar una vista
4. Agregar un modelo
5. Trabajar con SQL Server LocalDB
6. Vistas y métodos de controlador
7. Agregar búsqueda
8. Agregar un campo nuevo
9. Agregar validación
10. Examinar los métodos Details y Delete
Introducción a ASP.NET Core MVC y Visual Studio
17/05/2018 • 6 minutes to read • Edit Online

Por Rick Anderson


En este tutorial se muestra el desarrollo web de ASP.NET Core MVC con controladores y vistas. Las páginas de
Razor son una nueva alternativa en ASP.NET Core 2.0 y posteriores, un modelo de programación basado en
páginas que facilita la compilación de interfaces de usuario web y hace que sean más productivas. Se recomienda
probar el tutorial de las páginas de Razor antes que la versión MVC. El tutorial de las páginas de Razor:
Es el método preferido para el desarrollo de nuevas aplicaciones.
Es más fácil de seguir.
Abarca más características.
Si elige este tutorial en lugar de la versión de páginas de Razor, le agradeceremos que nos explique el motivo en
este problema de GitHub.
Hay tres versiones de este tutorial:
macOS: Creación de una aplicación de ASP.NET Core MVC con Visual Studio para Mac
Windows: Creación de una aplicación de ASP.NET Core MVC con Visual Studio
macOS, Linux y Windows: Creación de una aplicación de ASP.NET Core MVC con Visual Studio Code

Instalar Visual Studio y .NET Core


ASP.NET Core 2.x
ASP.NET Core 1.x
Install one of the following:
CLI tooling: Windows, Linux, or macOS: .NET Core SDK 2.0 or later
IDE/editor tooling
Windows: Visual Studio for Windows
ASP.NET and web development workload
.NET Core cross-platform development workload
Linux: Visual Studio Code
macOS: Visual Studio for Mac

Crear una aplicación web


En Visual Studio, seleccione Archivo > Nuevo > Proyecto.
Complete el cuadro de diálogo Nuevo proyecto:
En el panel izquierdo, pulse .NET Core.
En el panel central, pulse Aplicación web ASP.NET Core (.NET Core).
Asigne al proyecto el nombre "MvcMovie" (es importante asignarle este nombre para que, al copiar el código,
coincida con el espacio de nombres).
Pulse Aceptar.

ASP.NET Core 2.x


ASP.NET Core 1.x
Complete el cuadro de diálogo Nueva aplicación web ASP.NET Core (.NET Core) - MvcMovie:
En el cuadro desplegable del selector de versión, seleccione ASP.NET Core 2.-.
Seleccione Aplicación web (controlador de vista de modelos).
Pulse Aceptar.
Visual Studio ha usado una plantilla predeterminada para el proyecto de MVC que acaba de crear. Si escribe un
nombre de proyecto y selecciona algunas opciones, dispondrá de inmediato de una aplicación operativa. Se trata
de un proyecto introductorio sencillo, pero es un buen punto de partida.
Pulse F5 para ejecutar la aplicación en modo de depuración o Ctrl-F5 para ejecutarla en modo de no depuración.

Visual Studio inicia IIS Express y ejecuta la aplicación. Tenga en cuenta que en la barra de direcciones aparece
localhost:port# (y no algo como example.com ). Esto es así porque localhost es el nombre de host estándar
del equipo local. Cuando Visual Studio crea un proyecto web, se usa un puerto aleatorio para el servidor web.
En la imagen anterior, el número de puerto es el 5000. La dirección URL del explorador es localhost:5000 . Al
ejecutar la aplicación verá otro puerto distinto.
Iniciar la aplicación con CTRL+F5 (modo de no depuración) le permite efectuar cambios en el código,
guardar el archivo, actualizar el explorador y ver los cambios de código. Muchos desarrolladores prefieren
usar el modo de no depuración para iniciar la aplicación rápidamente y ver los cambios.
Puede iniciar la aplicación en modo de depuración o en modo de no depuración desde el elemento de menú
Depurar:

Puede depurar la aplicación pulsando el botón IIS Express.

La plantilla predeterminada proporciona los vínculos Inicio, Acerca de y Contacto, totalmente funcionales. En
la imagen del explorador anterior no se muestran estos vínculos. Según el tamaño del explorador, tendrá que
hacer clic en el icono de navegación para que se muestren.
Si iba a efectuar una ejecución en modo de depuración, pulse MAYÚS -F5 para detener la depuración.
En la siguiente sección de este tutorial obtendrá información sobre MVC y empezará a escribir código.

S IG U IE N T E
Agregar un controlador a una aplicación de ASP.NET
Core MVC
25/06/2018 • 10 minutes to read • Edit Online

Por Rick Anderson


El patrón de arquitectura del controlador de vista de modelos (MVC ) separa una aplicación en tres componentes
principales: Modelo, Vista y Controlador. El patrón de MVC ayuda a crear aplicaciones que son más fáciles de
actualizar y probar que las tradicionales aplicaciones monolíticas. Las aplicaciones basadas en MVC contienen:
Modelos: clases que representan los datos de la aplicación. Las clases de modelo usan lógica de validación
para aplicar las reglas de negocio para esos datos. Normalmente, los objetos de modelo recuperan y
almacenan el estado del modelo en una base de datos. En este tutorial, un modelo Movie recupera datos
de películas de una base de datos, los proporciona a la vista o los actualiza. Los datos actualizados se
escriben en una base de datos.
Vistas: las vistas son los componentes que muestran la interfaz de usuario de la aplicación. Por lo general,
esta interfaz de usuario muestra los datos del modelo.
Controladores: las clases que controlan las solicitudes del explorador. Recuperan los datos del modelo y
llaman a plantillas de vistas que devuelven una respuesta. En una aplicación MVC, la vista solo muestra
información; el controlador controla la interacción de los usuarios y los datos que introducen, y responde a
ellos. Por ejemplo, el controlador controla los datos de enrutamiento y los valores de cadena de consulta y
pasa estos valores al modelo. El modelo puede usar estos valores para consultar la base de datos. Por
ejemplo, http://localhost:1234/Home/About tiene datos de enrutamiento de Home (el controlador) y About
(el método de acción para llamar al controlador de inicio). http://localhost:1234/Movies/Edit/5 es una
solicitud para editar la película con ID=5 mediante el controlador de películas. Hablaremos sobre los datos
de enrutamiento más adelante en este tutorial.
El patrón de MVC ayuda a crear aplicaciones que separan los diferentes aspectos de la aplicación (lógica de
entrada, lógica comercial y lógica de la interfaz de usuario), a la vez que proporciona un acoplamiento vago entre
estos elementos. El patrón especifica dónde debe ubicarse cada tipo de lógica en la aplicación. La lógica de la
interfaz de usuario pertenece a la vista. La lógica de entrada pertenece al controlador. La lógica de negocios
pertenece al modelo. Esta separación ayuda a administrar la complejidad al compilar una aplicación, ya que
permite trabajar en uno de los aspectos de la implementación a la vez sin influir en el código de otro. Por ejemplo,
puede trabajar en el código de vista sin depender del código de lógica de negocios.
En esta serie de tutoriales se tratarán estos conceptos y se mostrará cómo usarlos para crear una aplicación de
película. El proyecto de MVC contiene carpetas para controladores y vistas.
En el Explorador de soluciones, haga clic con el botón derecho en Controladores > Agregar > Nuevo
elemento.
Seleccione Seleccionar controlador.
En el cuadro de diálogo Agregar nuevo elemento, escriba HelloWorldController.

Reemplace el contenido de Controllers/HelloWorldController.cs con lo siguiente:


using Microsoft.AspNetCore.Mvc;
using System.Text.Encodings.Web;

namespace MvcMovie.Controllers
{
public class HelloWorldController : Controller
{
//
// GET: /HelloWorld/

public string Index()


{
return "This is my default action...";
}

//
// GET: /HelloWorld/Welcome/

public string Welcome()


{
return "This is the Welcome action method...";
}
}
}

Cada método public en un controlador puede ser invocado como un punto de conexión HTTP. En el ejemplo
anterior, ambos métodos devuelven una cadena. Observe los comentarios delante de cada método.
Un extremo HTTP es una dirección URL que se puede usar como destino en la aplicación web, como por ejemplo
http://localhost:1234/HelloWorld . Combina el protocolo usado HTTP , la ubicación de red del servidor web
(incluido el puerto TCP ) localhost:1234 y el URI de destino HelloWorld .
El primer comentario dice que se trata de un método HTTP GET que se invoca mediante la anexión de
"/HelloWorld/" a la dirección URL base. El segundo comentario dice que se trata de un método HTTP GET que se
invoca mediante la anexión de "/HelloWorld/Welcome/" a la dirección URL. Más adelante en el tutorial usaremos
el motor de scaffolding para generar métodos HTTP POST .
Ejecute la aplicación en modo de no depuración y anexione "HelloWorld" a la ruta de acceso en la barra de
direcciones. El método Index devuelve una cadena.

MVC invoca las clases del controlador (y los métodos de acción que contienen) en función de la URL entrante. La
lógica de enrutamiento de URL predeterminada que usa MVC emplea un formato similar al siguiente para
determinar qué código se debe invocar:
/[Controller]/[ActionName]/[Parameters]

El formato para el enrutamiento se establece en el método Configure en Startup.cs.


app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});

Cuando se ejecuta la aplicación y no se suministra ningún segmento de dirección URL, de manera


predeterminada se usa el controlador "Home" y el método "Index" especificados en la línea de plantilla resaltada
arriba.
El primer segmento de dirección URL determina la clase de controlador que se va a ejecutar. De modo que
localhost:xxxx/HelloWorld se asigna a la clase HelloWorldController . La segunda parte del segmento de dirección
URL determina el método de acción en la clase. De modo que localhost:xxxx/HelloWorld/Index podría provocar
que se ejecute el método Index de la clase HelloWorldController . Tenga en cuenta que solo es necesario navegar
a localhost:xxxx/HelloWorld para que se llame al método Index de manera predeterminada. Esto es porque
Index es el método predeterminado al que se llamará en un controlador si no se especifica explícitamente un
nombre de método. La tercera parte del segmento de dirección URL ( id ) es para los datos de ruta. Veremos los
datos de ruta más adelante en este tutorial.
Vaya a http://localhost:xxxx/HelloWorld/Welcome . El método Welcome se ejecuta y devuelve la cadena "This is the
Welcome action method..." (Este es el método de acción de bienvenida). Para esta dirección URL, el controlador es
HelloWorld y Welcome es el método de acción. Todavía no ha usado el elemento [Parameters] de la dirección
URL.

Modifique el código para pasar cierta información del parámetro desde la dirección URL al controlador. Por
ejemplo: /HelloWorld/Welcome?name=Rick&numtimes=4 . Cambie el método Welcome para que incluya dos parámetros,
como se muestra en el código siguiente.

// GET: /HelloWorld/Welcome/
// Requires using System.Text.Encodings.Web;
public string Welcome(string name, int numTimes = 1)
{
return HtmlEncoder.Default.Encode($"Hello {name}, NumTimes is: {numTimes}");
}

El código anterior:
Usa la característica de parámetro opcional de C# para indicar que el parámetro numTimes tiene el valor
predeterminado 1 si no se pasa ningún valor para ese parámetro.
Usa HtmlEncoder.Default.Encode para proteger la aplicación de entradas malintencionadas (es decir,
JavaScript).
Usa cadenas interpoladas.
Ejecute la aplicación y navegue a:
http://localhost:xxxx/HelloWorld/Welcome?name=Rick&numtimes=4

(Reemplace xxxx con el número de puerto). Puede probar valores diferentes para name y numtimes en la dirección
URL. El sistema de enlace de modelos de MVC asigna automáticamente los parámetros con nombre de la cadena
de consulta en la barra de dirección a los parámetros del método. Vea Model Binding (Enlace de modelos) para
más información.

En la ilustración anterior, el segmento de dirección URL ( Parameters ) no se usa, y los parámetros name y
numTimes se pasan como cadenas de consulta. El ? (signo de interrogación) en la dirección URL anterior es un
separador y le siguen las cadenas de consulta. El carácter & separa las cadenas de consulta.
Reemplace el método Welcome con el código siguiente:

public string Welcome(string name, int ID = 1)


{
return HtmlEncoder.Default.Encode($"Hello {name}, ID: {ID}");
}

Ejecute la aplicación y escriba la dirección URL siguiente: http://localhost:xxx/HelloWorld/Welcome/3?name=Rick

Esta vez el tercer segmento de dirección URL coincide con el parámetro de ruta id . El método Welcome contiene
un parámetro id que coincide con la plantilla de dirección URL en el método MapRoute . El ? del final (en id? )
indica que el parámetro id es opcional.
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});

En estos ejemplos, el controlador ha realizado la parte "VC" de MVC, es decir, el trabajo de vista y de controlador.
El controlador devuelve HTML directamente. Por lo general, no es aconsejable que los controles devuelvan HTML
directamente, porque resulta muy complicado de programar y mantener. En su lugar, se suele usar un archivo de
plantilla de vista de Razor independiente para ayudar a generar la respuesta HTML. Haremos esto en el siguiente
tutorial.
En el modo de no depuración (Ctrl+F5) de Visual Studio no es necesario compilar la aplicación después de
cambiar el código. Solo tiene que guardar el archivo y actualizar el explorador para ver los cambios.

A N T E R IO R S IG U IE N T E
Adición de una vista en una aplicación de ASP.NET
Core MVC
25/06/2018 • 14 minutes to read • Edit Online

Por Rick Anderson


En esta sección, se modificará la clase HelloWorldController para usar los archivos de plantilla de vista Razor con
el objetivo de encapsular correctamente el proceso de generar respuestas HTML a un cliente.
Para crear un archivo de plantilla de vista se usa Razor. Las plantillas de vista basadas en Razor tienen una
extensión de archivo .cshtml. Ofrecen una forma elegante de crear un resultado HTML mediante C#.
Actualmente, el método Index devuelve una cadena con un mensaje que está codificado de forma rígida en la
clase de controlador. En la clase HelloWorldController , reemplace el método Index por el siguiente código:

public IActionResult Index()


{
return View();
}

El código anterior devuelve un objeto View . Usa una plantilla de vista para generar una respuesta HTML al
explorador. Los métodos de controlador (también conocidos como métodos de acción), como el método Index
anterior, suelen devolver IActionResult o una clase derivada de ActionResult , en lugar de un tipo como una
cadena.
Haga clic con el botón derecho en la carpeta Vistas, haga clic en Agregar > Nueva carpeta y asigne a la
carpeta el nombre HelloWorld.
Haga clic con el botón derecho en la carpeta Views/HelloWorld y, luego, haga clic en Agregar > Nuevo
elemento.
En el cuadro de diálogo Agregar nuevo elemento - MvcMovie
En el cuadro de búsqueda situado en la esquina superior derecha, escriba Vista.
Pulse Vista de Razor.
En el cuadro Nombre, cambie el nombre si es necesario por Index.cshtml.
Pulse Agregar.
Reemplace el contenido del archivo de vista de Razor Views/HelloWorld/Index.cshtml con lo siguiente:

@{
ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>Hello from our View Template!</p>

Navegue a http://localhost:xxxx/HelloWorld . El método Index en HelloWorldController no hizo mucho; ejecutó


la instrucción return View(); , que especificaba que el método debe usar un archivo de plantilla de vista para
representar una respuesta al explorador. Como no especificó expresamente el nombre del archivo de plantilla de
vista, MVC usó de manera predeterminada el archivo de vista Index.cshtml de la carpeta /Views/HelloWorld. La
imagen siguiente muestra la cadena "Hello from our View Template!" (Hola desde nuestra plantilla de vista)
codificada de forma rígida en la vista.

Si la ventana del explorador es pequeña (por ejemplo en un dispositivo móvil), es conveniente que alterne (pulse)
el botón de navegación de arranque en la esquina superior derecha para ver los vínculos Home (Inicio), About
(Acerca de) y Contact (Contacto).
Cambiar vistas y páginas de diseño
Pulse los vínculos de menú: MvcMovie (Película de MVC ), Home (Inicio), About (Acerca de). Cada página
muestra el mismo diseño de menú. El diseño de menú se implementa en el archivo Views/Shared/_Layout.cshtml.
Abra el archivo Views/Shared/_Layout.cshtml.
Las plantillas de diseño permiten especificar el diseño del contenedor HTML del sitio en un solo lugar y, después,
aplicarlo en varias páginas del sitio. Busque la línea @RenderBody() . RenderBody es un marcador de posición
donde se mostrarán todas las páginas específicas de vista que cree, encapsuladas en la página de diseño. Por
ejemplo, si selecciona el vínculo About (Acerca de), la vista Views/Home/About.cshtml se representa dentro
del método RenderBody .

Cambiar el título y el vínculo del menú en el archivo de diseño


En el elemento de título, cambie MvcMovie por Movie App . Cambie el texto del delimitador en la plantilla de
diseño de MvcMovie a Movie App y el controlador de Home a Movies como se resalta aquí:

[!code-html]
[!code-html]

WARNING
Aún no hemos implementado el controlador Movies , por lo que si hace clic en ese vínculo, obtendrá un error 404 (no
encontrado).

Guarde los cambios y pulse en el vínculo About (Acerca de). Observe cómo el título de la pestaña del explorador
muestra ahora About - Movie App (Acerca de - Aplicación de película) en lugar de About - Mvc Movie (Acerca
de - Aplicación de MVC ):
Pulse el vínculo Contacto y observe que el texto del título y el delimitador también muestran Movie App.
Hemos realizado el cambio una vez en la plantilla de diseño y hemos conseguido que todas las páginas del sitio
reflejen el nuevo texto de vínculo y el nuevo título.
Examine el archivo Views/_ViewStart.cshtml:

@{
Layout = "_Layout";
}

El archivo Views/_ViewStart.cshtml trae el archivo Views/Shared/_Layout.cshtml a cada vista. Puede usar la


propiedad Layout para establecer una vista de diseño diferente o establecerla en null para que no se use
ningún archivo de diseño.
Cambie el título de la vista Index .
Abra Views/HelloWorld/Index.cshtml. Los cambios se pueden hacer en dos lugares:
El texto que aparece en el título del explorador.
El encabezado secundario (elemento <h2> ).

Haremos que sean algo diferentes para que pueda ver qué parte del código cambia cada área de la aplicación.

@{
ViewData["Title"] = "Movie List";
}

<h2>My Movie List</h2>

<p>Hello from our View Template!</p>

En el código anterior, ViewData["Title"] = "Movie List"; establece la propiedad Title del diccionario ViewData
en "Movie List" (Lista de películas). La propiedad Title se usa en el elemento HTML <title> en la página de
diseño:

<title>@ViewData["Title"] - Movie App</title>

Guarde el cambio y navegue a http://localhost:xxxx/HelloWorld . Tenga en cuenta que el título del explorador, el
encabezado principal y los encabezados secundarios han cambiado. (Si no ve los cambios en el explorador, es
posible que esté viendo contenido almacenado en caché. Presione Ctrl+F5 en el explorador para forzar que se
cargue la respuesta del servidor). El título del explorador se crea con ViewData["Title"] , que se definió en la
plantilla de vista Index.cshtml y el texto "- Movie App" (-Aplicación de película) que se agregó en el archivo de
diseño.
Observe también cómo el contenido de la plantilla de vista Index.cshtml se fusionó con la plantilla de vista
Views/Shared/_Layout.cshtml y se envió una única respuesta HTML al explorador. Con las plantillas de diseño es
realmente fácil hacer cambios para que se apliquen en todas las páginas de la aplicación. Para saber más, vea
Layout (Diseño).

Nuestra pequeña cantidad de "datos", en este caso, el mensaje "Hello from our View Template!" (Hola desde
nuestra plantilla de vista), están codificados de forma rígida. La aplicación de MVC tiene una "V" (vista) y ha
obtenido una "C" (controlador), pero todavía no tiene una "M" (modelo).

Pasar datos del controlador a la vista


Las acciones del controlador se invocan en respuesta a una solicitud de dirección URL entrante. Una clase de
controlador es donde se escribe el código que controla las solicitudes entrantes del explorador. El controlador
recupera datos de un origen de datos y decide qué tipo de respuesta devolverá al explorador. Las plantillas de
vista se pueden usar desde un controlador para generar y dar formato a una respuesta HTML al explorador.
Los controladores se encargan de proporcionar los datos necesarios para que una plantilla de vista represente una
respuesta. Procedimiento recomendado: las plantillas de vista no deben realizar lógica de negocios ni interactuar
directamente con una base de datos. En su lugar, una plantilla de vista debe funcionar solo con los datos que le
proporciona el controlador. Mantener esta "separación de intereses" ayuda a mantener el código limpio, fácil de
probar y de mantener.
Actualmente, el método Welcome de la clase HelloWorldController toma un parámetro name y ID , y luego
obtiene los valores directamente en el explorador. En lugar de que el controlador represente esta respuesta como
una cadena, cambie el controlador para que use una plantilla de vista. La plantilla de vista genera una respuesta
dinámica, lo que significa que se deben pasar las partes de datos adecuadas desde el controlador a la vista para
que se genere la respuesta. Para hacerlo, indique al controlador que coloque los datos dinámicos (parámetros)
que necesita la plantilla de vista en un diccionario ViewData al que luego pueda obtener acceso la plantilla de
vista.
Vuelva al archivo HelloWorldController.cs y cambie el método Welcome para agregar un valor Message y
NumTimes al diccionario ViewData . El diccionario ViewData es un objeto dinámico, lo que significa que puede
colocar en él todo lo que quiera; el objeto ViewData no tiene ninguna propiedad definida hasta que coloca algo
dentro de él. El sistema de enlace de modelos de MVC asigna automáticamente los parámetros con nombre (
name y numTimes ) de la cadena de consulta en la barra de dirección a los parámetros del método. El archivo
HelloWorldController.cs completo tiene este aspecto:

using Microsoft.AspNetCore.Mvc;
using System.Text.Encodings.Web;

namespace MvcMovie.Controllers
{
public class HelloWorldController : Controller
{
public IActionResult Index()
{
return View();
}

public IActionResult Welcome(string name, int numTimes = 1)


{
ViewData["Message"] = "Hello " + name;
ViewData["NumTimes"] = numTimes;

return View();
}
}
}

El objeto de diccionario ViewData contiene datos que se pasarán a la vista.


Cree una plantilla de vista principal denominada Views/HelloWorld/Welcome.cshtml.
Se creará un bucle en la vista Welcome.cshtml que muestra "Hello" (Hola) NumTimes . Reemplace el contenido de
Views/HelloWorld/Welcome.cshtml con lo siguiente:

@{
ViewData["Title"] = "Welcome";
}

<h2>Welcome</h2>

<ul>
@for (int i = 0; i < (int)ViewData["NumTimes"]; i++)
{
<li>@ViewData["Message"]</li>
}
</ul>

Guarde los cambios y vaya a esta dirección URL:


http://localhost:xxxx/HelloWorld/Welcome?name=Rick&numtimes=4

Los datos se toman de la dirección URL y se pasan al controlador mediante el enlazador de modelos MVC. El
controlador empaqueta los datos en un diccionario ViewData y pasa ese objeto a la vista. Después, la vista
representa los datos como HTML en el explorador.
En el ejemplo anterior, usamos el diccionario ViewData para pasar datos del controlador a una vista. Más adelante
en el tutorial usaremos un modelo de vista para pasar datos de un controlador a una vista. El enfoque del modelo
de vista que consiste en pasar datos suele ser más preferible que el enfoque de diccionario ViewData . Para saber
más, vea ViewModel vs ViewData vs ViewBag vs TempData vs Session in MVC (ViewModel, ViewData, ViewBag,
TempData y Session en MVC ).
Bueno, todo esto era un tipo de "M" para el modelo, pero no el tipo de base de datos. Vamos a aprovechar lo que
hemos aprendido para crear una base de datos de películas.

A N T E R IO R S IG U IE N T E
Agregar un modelo a una aplicación de ASP.NET
Core MVC
14/05/2018 • 15 minutes to read • Edit Online

Adición de un modelo a una aplicación de ASP.NET Core


MVC
Por Rick Anderson y Tom Dykstra
En esta sección, agregará algunas clases para administrar películas en una base de datos. Estas clases serán el
elemento "Model" de la aplicación MVC.
Estas clases se usan con Entity Framework Core (EF Core) para trabajar con una base de datos. EF Core es un
marco de trabajo de asignación relacional de objetos (ORM ) que simplifica el código de acceso de datos que se
debe escribir. EF Core es compatible con muchos motores de base de datos.
Las clases de modelo que se crean se conocen como clases POCO (del inglés "plain-old CLR objects", objetos
CLR antiguos sin formato) porque no tienen ninguna dependencia de EF Core. Simplemente definen las
propiedades de los datos que se almacenan en la base de datos.
En este tutorial, se escriben primero las clases de modelo y EF Core creará la base de datos. Hay un enfoque
alternativo, que no se trata aquí, que consiste en generar clases de modelo a partir de una base de datos existente.
Para más información sobre este enfoque, vea ASP.NET Core - Existing Database (ASP.NET Core - base de datos
existente).

Agregar una clase de modelo de datos


Nota: Las plantillas de ASP.NET Core 2.0 contienen la carpeta Models.
Haga clic con el botón derecho en la carpeta Models > Agregar > Clase. Asigne a la clase el nombre Movie y
agregue las siguientes propiedades:

using System;

namespace MvcMovie.Models
{
public class Movie
{
public int ID { get; set; }
public string Title { get; set; }
public DateTime ReleaseDate { get; set; }
public string Genre { get; set; }
public decimal Price { get; set; }
}
}

La base de datos requiere el campo ID para la clave principal.


Compile el proyecto para comprobar que no contiene errores. Ahora tiene un modelo en la aplicación MVC.

Scaffolding de un controlador
En el Explorador de soluciones, haga clic con el botón derecho en la carpeta Controladores > Agregar >
Controlador.

Si aparece el cuadro de diálogo Agregar dependencias de MVC:


Actualice Visual Studio a la última versión. La versiones de Visual Studio anteriores a la 15.5 muestran este
cuadro de diálogo.
Si no puede actualizar, seleccione AGREGAR y luego siga los pasos para agregar el controlador de nuevo.
En el cuadro de diálogo Agregar scaffold, pulse en Controlador de MVC con vistas que usan Entity
Framework > Agregar.

Rellene el cuadro de diálogo Agregar controlador:


Clase de modelo: Movie (MvcMovie.Models).
Clase de contexto de datos: seleccione el icono + y agregue el valor predeterminado
MvcMovie.Models.MvcMovieContext.

Vistas: conserve el valor predeterminado de cada opción activada.


Nombre del controlador: conserve el valor predeterminado MoviesController.
Pulse en Agregar.

Visual Studio crea:


Una clase de contexto de base de datos de Entity Framework Core (Data/MvcMovieContext.cs)
Un controlador de películas (Controllers/MoviesController.cs)
Archivos de vistas Razor para las páginas de creación, eliminación, detalles, edición e índice
(Views/Movies/*.cshtml)

La creación automática del contexto de base de datos y de vistas y métodos de acción CRUD (crear, leer, actualizar
y eliminar) se conoce como scaffolding. Pronto contará con una aplicación web totalmente funcional que le
permitirá administrar una base de datos de películas.
Si ejecuta la aplicación y hace clic en el vínculo Mvc Movie, aparece un error similar al siguiente:
An unhandled exception occurred while processing the request.

SqlException: Cannot open database "MvcMovieContext-<GUID removed>" requested by the login. The login failed.
Login failed for user 'Rick'.

System.Data.SqlClient.SqlInternalConnectionTds..ctor(DbConnectionPoolIdentity identity, SqlConnectionString

Debe crear la base de datos y para ello usar la característica Migraciones de EF Core. Las migraciones permiten
crear una base de datos que coincide con el modelo de datos y actualizan el esquema de base de datos cuando
cambia el modelo de datos.

Adición de herramientas de EF y migración inicial


En esta sección se usa la Consola del Administrador de paquetes (PMC ) para:
Agregar el paquete de herramientas de Entity Framework Core. Este paquete es necesario para agregar
migraciones y actualizar la base de datos.
Agregar una migración inicial.
Actualizar la base de datos con la migración inicial.
En el menú Herramientas, seleccione Administrador de paquetes NuGet > Consola del Administrador de
paquetes.

En PCM, escriba los siguientes comandos:

Install-Package Microsoft.EntityFrameworkCore.Tools
Add-Migration Initial
Update-Database

Nota: Si recibe un error con el comando Install-Package , abra el administrador de paquetes NuGet y busque el
paquete Microsoft.EntityFrameworkCore.Tools . De esta forma, podrá instalar el paquete o comprobar si ya está
instalado. Como alternativa, vea el enfoque de la CLI si tiene problemas con la PMC.
El comando Add-Migration genera el código para crear el esquema de base de datos inicial. El esquema se basa
en el modelo especificado en DbContext (en el archivo Data/MvcMovieContext.cs). El argumento Initial se usa
para asignar nombre a las migraciones. Puede usar cualquier nombre, pero se suele elegir uno que describa la
migración. Vea Introduction to migrations (Introducción a las migraciones) para obtener más información.
El comando Update-Database ejecuta el método Up en el archivo Migrations/<marca_de_tiempo>_Initial.cs, con
lo que se crea la base de datos.
Puede realizar los pasos anteriores mediante la interfaz de línea de comandos (CLI) en lugar de la PMC:
Agregue las herramientas de EF Core al archivo .csproj.
Ejecute los siguientes comandos desde la consola (en el directorio del proyecto):

dotnet ef migrations add Initial


dotnet ef database update

Si ejecuta la aplicación y aparece el error:

SqlException: Cannot open database "Movie" requested by the login.


The login failed.
Login failed for user 'user name'.

Probablemente se deba a que no ha ejecutado dotnet ef database update .

Prueba de la aplicación
Ejecute la aplicación y pulse en el vínculo Mvc Movie (Película Mvc).
Pulse Create New (Crear nueva) y cree una película.
Es posible no que pueda escribir comas ni puntos decimales en el campo Price . Para admitir la validación
de jQuery para configuraciones regionales distintas del inglés que usan una coma (",") en lugar de un
punto decimal y formatos de fecha distintos de Estados Unidos, debe seguir unos pasos globalizar la
aplicación. Consulte https://github.com/aspnet/Docs/issues/4076 y recursos adicionales para obtener más
información. Por ahora, tan solo debe escribir números enteros como 10.
En algunas configuraciones regionales debe especificar el formato de fecha. Vea el código que aparece
resaltado a continuación.

using System;
using System.ComponentModel.DataAnnotations;

namespace MvcMovie.Models
{
public class Movie
{
public int ID { get; set; }
public string Title { get; set; }
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime ReleaseDate { get; set; }
public string Genre { get; set; }
public decimal Price { get; set; }
}
}
Hablaremos sobre DataAnnotations más adelante en el tutorial.
Al pulsar en Crear el formulario se envía al servidor, donde se guarda la información de la película en una base de
datos. La aplicación redirige a la URL /Movies, donde se muestra la información de la película que acaba de crear.

Cree un par de entradas más de película. Compruebe que todos los vínculos Editar, Detalles y Eliminar son
funcionales.

public void ConfigureServices(IServiceCollection services)


{
// Add framework services.
services.AddMvc();

services.AddDbContext<MvcMovieContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("MvcMovieContext")));
}

En el código resaltado anterior se muestra cómo se agrega el contexto de la base de datos de películas al
contenedor inserción de dependencias (en el archivo Startup.cs).
services.AddDbContext<MvcMovieContext>(options => especifica la base de datos que se usará y la cadena de
conexión. => es un operador lambda.
Abra el archivo Controllers/MoviesController.cs y examine el constructor:

public class MoviesController : Controller


{
private readonly MvcMovieContext _context;

public MoviesController(MvcMovieContext context)


{
_context = context;
}

El constructor usa la inserción de dependencias para insertar el contexto de base de datos ( MvcMovieContext ) en el
controlador. El contexto de base de datos se usa en cada uno de los métodos CRUD del controlador.

Modelos fuertemente tipados y la palabra clave @model


Anteriormente en este tutorial, vimos cómo un controlador puede pasar datos u objetos a una vista mediante el
diccionario ViewData . El diccionario ViewData es un objeto dinámico que proporciona una cómoda manera
enlazada en tiempo de ejecución de pasar información a una vista.
MVC también ofrece la capacidad de pasar objetos de modelo fuertemente tipados a una vista. Este enfoque
fuertemente tipado permite una mejor comprobación del código en tiempo de compilación. El mecanismo de
scaffolding usó este enfoque (que consiste en pasar un modelo fuertemente tipado) con la clase MoviesController
y las vistas cuando creó los métodos y las vistas.
Examine el método Details generado en el archivo Controllers/MoviesController.cs:

// GET: Movies/Details/5
public async Task<IActionResult> Details(int? id)
{
if (id == null)
{
return NotFound();
}

var movie = await _context.Movie


.SingleOrDefaultAsync(m => m.ID == id);
if (movie == null)
{
return NotFound();
}

return View(movie);
}

El parámetro id suele pasarse como datos de ruta. Por ejemplo, http://localhost:5000/movies/details/1


establece:
El controlador en el controlador movies (el primer segmento de dirección URL ).
La acción en details (el segundo segmento de dirección URL ).
El identificador en 1 (el último segmento de dirección URL ).
También puede pasar id con una cadena de consulta como se indica a continuación:
http://localhost:1234/movies/details?id=1

El parámetro id se define como un tipo que acepta valores NULL ( int? ) en caso de que no se proporcione un
valor de identificador.
Se pasa una expresión lambda a SingleOrDefaultAsync para seleccionar entidades de película que coincidan con
los datos de enrutamiento o el valor de consulta de cadena.

var movie = await _context.Movie


.SingleOrDefaultAsync(m => m.ID == id);

Si se encuentra una película, se pasa una instancia del modelo Movie a la vista Details :

return View(movie);

Examine el contenido del archivo Views/Movies/Details.cshtml:


@model MvcMovie.Models.Movie

@{
ViewData["Title"] = "Details";
}

<h2>Details</h2>

<div>
<h4>Movie</h4>
<hr />
<dl class="dl-horizontal">
<dt>
@Html.DisplayNameFor(model => model.Title)
</dt>
<dd>
@Html.DisplayFor(model => model.Title)
</dd>
<dt>
@Html.DisplayNameFor(model => model.ReleaseDate)
</dt>
<dd>
@Html.DisplayFor(model => model.ReleaseDate)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Genre)
</dt>
<dd>
@Html.DisplayFor(model => model.Genre)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Price)
</dt>
<dd>
@Html.DisplayFor(model => model.Price)
</dd>
</dl>
</div>
<div>
<a asp-action="Edit" asp-route-id="@Model.ID">Edit</a> |
<a asp-action="Index">Back to List</a>
</div>

Mediante la inclusión de una instrucción @model en la parte superior del archivo de vista, puede especificar el tipo
de objeto que espera la vista. Cuando se creó el controlador de película, Visual Studio incluyó automáticamente la
siguiente instrucción @model en la parte superior del archivo Details.cshtml:

@model MvcMovie.Models.Movie

Esta directiva @model permite acceder a la película que el controlador pasó a la vista usando un objeto Model
fuertemente tipado. Por ejemplo, en la vista Details.cshtml, el código pasa cada campo de película a las
aplicaciones auxiliares HTML DisplayNameFor y DisplayFor con el objeto Model fuertemente tipado. Los
métodos Create y Edit y las vistas también pasan un objeto de modelo Movie .
Examine la vista Index.cshtml y el método Index en el controlador Movies. Observe cómo el código crea un
objeto List cuando llama al método View . El código pasa esta lista Movies desde el método de acción Index a
la vista:
// GET: Movies
public async Task<IActionResult> Index()
{
return View(await _context.Movie.ToListAsync());
}

Cuando se creó el controlador movies, el scaffolding incluyó automáticamente la siguiente instrucción @model en
la parte superior del archivo Index.cshtml:

@model IEnumerable<MvcMovie.Models.Movie>

Esta directiva @model permite acceder a la lista de películas que el controlador pasó a la vista usando un objeto
Model fuertemente tipado. Por ejemplo, en la vista Index.cshtml, el código recorre en bucle las películas con una
instrucción foreach sobre el objeto Model fuertemente tipado:
@model IEnumerable<MvcMovie.Models.Movie>

@{
ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Title)
</th>
<th>
@Html.DisplayNameFor(model => model.ReleaseDate)
</th>
<th>
@Html.DisplayNameFor(model => model.Genre)
</th>
<th>
@Html.DisplayNameFor(model => model.Price)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.ReleaseDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Genre)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
<a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-action="Details" asp-route-id="@item.ID">Details</a> |
<a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>

Como el objeto Model es fuertemente tipado (como un objeto IEnumerable<Movie> ), cada elemento del bucle está
tipado como Movie . Entre otras ventajas, esto significa que se obtiene una comprobación del código en tiempo de
compilación:
Recursos adicionales
Aplicaciones auxiliares de etiquetas
Globalización y localización

A N T E R IO R : A G R E G A R U N A S IG U IE N T E : T R A B A J A R C O N
V IS T A SQL
Trabajar con SQL Server LocalDB en ASP.NET Core
25/06/2018 • 4 minutes to read • Edit Online

Por Rick Anderson


El objeto MvcMovieContext controla la tarea de conexión a la base de datos y asignación de objetos Movie a los
registros de la base de datos. El contexto de base de datos se registra con el contenedor de inserción de
dependencias en el método ConfigureServices del archivo Startup.cs:
[!code-csharp]
[!code-csharp]
El sistema Configuración de ASP.NET Core lee el elemento ConnectionString . Para el desarrollo local, obtiene la
cadena de conexión del archivo appsettings.json:

"ConnectionStrings": {
"MvcMovieContext": "Server=(localdb)\\mssqllocaldb;Database=MvcMovieContext-
2;Trusted_Connection=True;MultipleActiveResultSets=true"
}

Al implementar la aplicación en un servidor de producción o de prueba, puede usar una variable de entorno u
otro enfoque para establecer la cadena de conexión en una instancia real de SQL Server. Para más información,
vea Configuración.

SQL Server Express LocalDB


LocalDB es una versión ligera del motor de base de datos de SQL Server Express dirigida al desarrollo de
programas. LocalDB se inicia a petición y se ejecuta en modo de usuario, sin necesidad de una configuración
compleja. De forma predeterminada, la base de datos LocalDB crea archivos "*.mdf" en el directorio
C:/Users/<usuario>.
En el menú Ver, abra Explorador de objetos de SQL Server (SSOX).

Haga clic con el botón derecho en la tabla Movie > Diseñador de vistas.
Observe el icono de llave junto a ID . De forma predeterminada, EF convierte una propiedad denominada ID en
la clave principal.
Haga clic con el botón derecho en la tabla Movie > Ver datos
Inicialización de la base de datos
Cree una nueva clase denominada SeedData en la carpeta Models. Reemplace el código generado con el
siguiente:
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Linq;

namespace MvcMovie.Models
{
public static class SeedData
{
public static void Initialize(IServiceProvider serviceProvider)
{
using (var context = new MvcMovieContext(
serviceProvider.GetRequiredService<DbContextOptions<MvcMovieContext>>()))
{
// Look for any movies.
if (context.Movie.Any())
{
return; // DB has been seeded
}

context.Movie.AddRange(
new Movie
{
Title = "When Harry Met Sally",
ReleaseDate = DateTime.Parse("1989-1-11"),
Genre = "Romantic Comedy",
Price = 7.99M
},

new Movie
{
Title = "Ghostbusters ",
ReleaseDate = DateTime.Parse("1984-3-13"),
Genre = "Comedy",
Price = 8.99M
},

new Movie
{
Title = "Ghostbusters 2",
ReleaseDate = DateTime.Parse("1986-2-23"),
Genre = "Comedy",
Price = 9.99M
},

new Movie
{
Title = "Rio Bravo",
ReleaseDate = DateTime.Parse("1959-4-15"),
Genre = "Western",
Price = 3.99M
}
);
context.SaveChanges();
}
}
}
}

Si hay alguna película en la base de datos, se devuelve el inicializador y no se agrega ninguna película.
if (context.Movie.Any())
{
return; // DB has been seeded.
}

Agregar el inicializador
[!code-csharp]
ASP.NET Core 2.x
ASP.NET Core 1.x
Agregue el inicializador al método Main en el archivo Program.cs:
[!code-csharp]
Prueba de la aplicación
Elimine todos los registros de la base de datos. Puede hacerlo con los vínculos de eliminación en el
explorador o desde SSOX.
Obligue a la aplicación a inicializarse (llame a los métodos de la clase Startup ) para que se ejecute el
método de inicialización. Para forzar la inicialización, se debe detener y reiniciar IIS Express. Puede hacerlo
con cualquiera de los siguientes enfoques:
Haga clic con el botón derecho en el icono Bandeja del sistema de IIS Express del área de
notificación y pulse en Salir o en Detener sitio.

Si está ejecutando VS en modo de no depuración, presione F5 para ejecutar en modo de


depuración
Si está ejecutando VS en modo de depuración, detenga el depurador y presione F5
La aplicación muestra los datos inicializados.
A N T E R IO R S IG U IE N T E
Vistas y métodos de controlador en ASP.NET Core
25/06/2018 • 14 minutes to read • Edit Online

Por Rick Anderson


La aplicación de películas pinta bien, pero la presentación no es ideal. No queremos ver la hora (12:00:00 a.m. en
la imagen siguiente) y ReleaseDate deben ser dos palabras.

Abra el archivo Models/Movie.cs y agregue las líneas resaltadas que se muestran a continuación:
[!code-csharp]
[!code-csharp]
En el próximo tutorial hablaremos de DataAnnotations. El atributo Display especifica qué se muestra como
nombre de un campo (en este caso, "Release Date" en lugar de "ReleaseDate"). El atributo DataType especifica el
tipo de los datos (Date), así que la información de hora almacenada en el campo no se muestra.
Vaya al controlador Movies y mantenga el puntero del mouse sobre un vínculo Edit (Editar) para ver la dirección
URL de destino.
Los vínculos Edit (Editar), Details (Detalles) y Delete (Eliminar) se generan mediante la aplicación auxiliar de
etiquetas de delimitador de MVC Core en el archivo Views/Movies/Index.cshtml.

<a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |


<a asp-action="Details" asp-route-id="@item.ID">Details</a> |
<a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>

Las aplicaciones auxiliares de etiquetas permiten que el código de servidor participe en la creación y la
representación de elementos HTML en archivos de Razor. En el código anterior, AnchorTagHelper genera
dinámicamente el valor del atributo HTML href a partir del identificador de ruta y el método de acción del
controlador. Use Ver código fuente en su explorador preferido o use las herramientas de desarrollo para
examinar el marcado generado. A continuación se muestra una parte del HTML generado:

<td>
<a href="/Movies/Edit/4"> Edit </a> |
<a href="/Movies/Details/4"> Details </a> |
<a href="/Movies/Delete/4"> Delete </a>
</td>

Recupere el formato para el enrutamiento establecido en el archivo Startup.cs:

app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});

ASP.NET Core traduce http://localhost:1234/Movies/Edit/4 en una solicitud al método de acción Edit del
controlador Movies con el parámetro Id de 4. (Los métodos de controlador también se denominan métodos de
acción).
Las aplicaciones auxiliares de etiquetas son una de las nuevas características más populares de ASP.NET Core.
Para más información, vea Recursos adicionales.
Abra el controlador Movies y examine los dos métodos de acción Edit . En el código siguiente se muestra el
método HTTP GET Edit , que captura la película y rellena el formulario de edición generado por el archivo de Razor
Edit.cshtml.

// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
return NotFound();
}

var movie = await _context.Movie.SingleOrDefaultAsync(m => m.ID == id);


if (movie == null)
{
return NotFound();
}
return View(movie);
}

En el código siguiente se muestra el método HTTP POST Edit , que procesa los valores de película publicados:

// POST: Movies/Edit/5
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("ID,Title,ReleaseDate,Genre,Price")] Movie movie)
{
if (id != movie.ID)
{
return NotFound();
}

if (ModelState.IsValid)
{
try
{
_context.Update(movie);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!MovieExists(movie.ID))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction("Index");
}
return View(movie);
}

El atributo [Bind] es una manera de proteger contra el exceso de publicación. Solo debe incluir propiedades en el
atributo [Bind] que quiera cambiar. Para más información, vea Protección del controlador frente al exceso de
publicación. ViewModels ofrece un enfoque alternativo para evitar el exceso de publicaciones.
Observe que el segundo método de acción Edit va precedido del atributo [HttpPost] .

// POST: Movies/Edit/5
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("ID,Title,ReleaseDate,Genre,Price")] Movie movie)
{
if (id != movie.ID)
{
return NotFound();
}

if (ModelState.IsValid)
{
try
{
_context.Update(movie);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!MovieExists(movie.ID))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction("Index");
}
return View(movie);
}

El atributo HttpPost especifica que este método Edit se puede invocar solamente para solicitudes POST . Podría
aplicar el atributo [HttpGet] al primer método de edición, pero no es necesario hacerlo porque [HttpGet] es el
valor predeterminado.
El atributo ValidateAntiForgeryToken se usa para impedir la falsificación de una solicitud y se empareja con un
token antifalsificación generado en el archivo de vista de edición (Views/Movies/Edit.cshtml). El archivo de vista de
edición genera el token antifalsificación con la aplicación auxiliar de etiquetas de formulario.

<form asp-action="Edit">

La aplicación auxiliar de etiquetas de formulario genera un token antifalsificación oculto que debe coincidir con el
token antifalsificación generado por [ValidateAntiForgeryToken] en el método Edit del controlador Movies. Para
más información, vea Prevención de ataques de falsificación de solicitudes.
El método HttpGet Edit toma el parámetro ID de la película, busca la película con el método
SingleOrDefaultAsync de Entity Framework y devuelve la película seleccionada a la vista de edición. Si no se
encuentra una película, se devuelve NotFound (HTTP 404).
// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
return NotFound();
}

var movie = await _context.Movie.SingleOrDefaultAsync(m => m.ID == id);


if (movie == null)
{
return NotFound();
}
return View(movie);
}

Cuando el sistema de scaffolding creó la vista de edición, examinó la clase Movie y creó código para representar
los elementos <label> y <input> para cada propiedad de la clase. En el ejemplo siguiente se muestra la vista de
edición que generó el sistema de scaffolding de Visual Studio:
@model MvcMovie.Models.Movie

@{
ViewData["Title"] = "Edit";
}

<h2>Edit</h2>

<form asp-action="Edit">
<div class="form-horizontal">
<h4>Movie</h4>
<hr />
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="ID" />
<div class="form-group">
<label asp-for="Title" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Title" class="form-control" />
<span asp-validation-for="Title" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="ReleaseDate" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="ReleaseDate" class="form-control" />
<span asp-validation-for="ReleaseDate" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="Genre" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Genre" class="form-control" />
<span asp-validation-for="Genre" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="Price" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Price" class="form-control" />
<span asp-validation-for="Price" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</div>
</div>
</form>

<div>
<a asp-action="Index">Back to List</a>
</div>

@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

Observe cómo la plantilla de vista tiene una instrucción @model MvcMovie.Models.Movie en la parte superior del
archivo. @model MvcMovie.Models.Movie especifica que la vista espera que el modelo de la plantilla de vista sea del
tipo Movie .
El código con scaffolding usa varios métodos de aplicación auxiliar de etiquetas para simplificar el marcado
HTML. La aplicación auxiliar de etiquetas muestra el nombre del campo: "Title" (Título), "ReleaseDate" (Fecha de
lanzamiento), "Genre" (Género) o "Price" (Precio). La aplicación auxiliar de etiquetas de entrada representa un
elemento HTML <input> . La aplicación auxiliar de etiquetas de validación muestra cualquier mensaje de
validación asociado a esa propiedad.
Ejecute la aplicación y navegue a la URL /Movies . Haga clic en un vínculo Edit (Editar). En el explorador, vea el
código fuente de la página. El código HTML generado para el elemento <form> se muestra abajo.

<form action="/Movies/Edit/7" method="post">


<div class="form-horizontal">
<h4>Movie</h4>
<hr />
<div class="text-danger" />
<input type="hidden" data-val="true" data-val-required="The ID field is required." id="ID" name="ID"
value="7" />
<div class="form-group">
<label class="control-label col-md-2" for="Genre" />
<div class="col-md-10">
<input class="form-control" type="text" id="Genre" name="Genre" value="Western" />
<span class="text-danger field-validation-valid" data-valmsg-for="Genre" data-valmsg-
replace="true"></span>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-2" for="Price" />
<div class="col-md-10">
<input class="form-control" type="text" data-val="true" data-val-number="The field Price must
be a number." data-val-required="The Price field is required." id="Price" name="Price" value="3.99" />
<span class="text-danger field-validation-valid" data-valmsg-for="Price" data-valmsg-
replace="true"></span>
</div>
</div>
<!-- Markup removed for brevity -->
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</div>
</div>
<input name="__RequestVerificationToken" type="hidden"
value="CfDJ8Inyxgp63fRFqUePGvuI5jGZsloJu1L7X9le1gy7NCIlSduCRx9jDQClrV9pOTTmqUyXnJBXhmrjcUVDJyDUMm7-
MF_9rK8aAZdRdlOri7FmKVkRe_2v5LIHGKFcTjPrWPYnc9AdSbomkiOSaTEg7RU" />
</form>

Los elementos <input> se muestran en un elemento HTML <form> cuyo atributo action se establece para
publicar en la dirección URL /Movies/Edit/id . Los datos del formulario se publicarán en el servidor cuando se
haga clic en el botón Save . La última línea antes del cierre del elemento </form> muestra el token XSRF oculto
generado por la aplicación auxiliar de etiquetas de formulario.

Procesamiento de la solicitud POST


En la siguiente lista se muestra la versión [HttpPost] del método de acción Edit .
// POST: Movies/Edit/5
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("ID,Title,ReleaseDate,Genre,Price")] Movie movie)
{
if (id != movie.ID)
{
return NotFound();
}

if (ModelState.IsValid)
{
try
{
_context.Update(movie);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!MovieExists(movie.ID))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction("Index");
}
return View(movie);
}

El atributo [ValidateAntiForgeryToken] valida el token XSRF oculto generado por el generador de tokens
antifalsificación en la herramienta auxiliar de etiquetas de formulario.
El sistema de enlace de modelos toma los valores de formulario publicados y crea un objeto Movie que se pasa
como el parámetro movie . El método ModelState.IsValid comprueba que los datos presentados en el formulario
pueden usarse para modificar (editar o actualizar) un objeto Movie . Si los datos son válidos, se guardan. Los
datos de película actualizados (o modificados) se guardan en la base de datos mediante una llamada al método
SaveChangesAsync del contexto de base de datos. Después de guardar los datos, el código redirige al usuario al
método de acción Index de la clase MoviesController , que muestra la colección de películas, incluidos los
cambios que se acaban de hacer.
Antes de que el formulario se envíe al servidor, la validación del lado cliente comprueba cualquier regla de
validación en los campos. Si hay errores de validación, se muestra un mensaje de error y no se publica el
formulario. Si JavaScript está deshabilitado, no dispondrá de la validación del lado cliente, sino que el servidor
detectará los valores publicados que no son válidos y los valores de formulario se volverán a mostrar con
mensajes de error. Más adelante en el tutorial se examina la validación de modelos con más detalle. La aplicación
auxiliar de etiquetas de validación en la plantilla de vista Views/Movies/Edit.cshtml se encarga de mostrar los
mensajes de error correspondientes.
Todos los métodos HttpGet del controlador de películas siguen un patrón similar. Obtienen un objeto de película
(o una lista de objetos, en el caso de Index ) y pasan el objeto (modelo) a la vista. El método Create pasa un
objeto de película vacío a la vista Create . Todos los métodos que crean, editan, eliminan o modifican los datos lo
hacen en la sobrecarga [HttpPost] del método. La modificación de datos en un método HTTP GET supone un
riesgo de seguridad. La modificación de datos en un método HTTP GET también infringe procedimientos
recomendados de HTTP y el patrón de arquitectura REST, que especifica que las solicitudes GET no deben
cambiar el estado de la aplicación. En otras palabras, realizar una operación GET debería ser una operación segura
sin efectos secundarios, que no modifica los datos persistentes.

Recursos adicionales
Globalización y localización
Introducción a las aplicaciones auxiliares de etiquetas
Creación de aplicaciones auxiliares de etiquetas
Prevención de ataques de falsificación de solicitudes
Protección del controlador frente al exceso de publicación
ViewModels
Aplicación auxiliar de etiquetas de formulario
Aplicación auxiliar de etiquetas de entrada
Aplicación auxiliar de etiquetas de elementos de etiqueta
Aplicación auxiliar de etiquetas de selección
Aplicación auxiliar de etiquetas de validación

A N T E R IO R S IG U IE N T E
Adición de una búsqueda en una aplicación de
ASP.NET Core MVC
25/06/2018 • 12 minutes to read • Edit Online

Por Rick Anderson


En esta sección agregará capacidad de búsqueda para el método de acción Index que permite buscar películas
por género o nombre.
Actualice el método Index con el código siguiente:

public async Task<IActionResult> Index(string searchString)


{
var movies = from m in _context.Movie
select m;

if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}

return View(await movies.ToListAsync());


}

La primera línea del método de acción Index crea una consulta LINQ para seleccionar las películas:

var movies = from m in _context.Movie


select m;

En este momento solo se define la consulta, no se ejecuta en la base de datos.


Si el parámetro searchString contiene una cadena, la consulta de películas se modifica para filtrar según el valor
de la cadena de búsqueda:

if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}

El código s => s.Title.Contains() anterior es una expresión Lambda. Las lambdas se usan en consultas LINQ
basadas en métodos como argumentos para métodos de operador de consulta estándar, tales como el método
Where o Contains (usado en el código anterior). Las consultas LINQ no se ejecutan cuando se definen ni cuando
se modifican mediante una llamada a un método, como Where , Contains u OrderBy . En su lugar, se aplaza la
ejecución de la consulta. Esto significa que la evaluación de una expresión se aplaza hasta que su valor realizado se
repita realmente o se llame al método ToListAsync . Para más información sobre la ejecución de consultas en
diferido, vea Ejecución de la consulta.
Nota: El método Contains se ejecuta en la base de datos, no en el código de c# que se muestra arriba. La
distinción entre mayúsculas y minúsculas en la consulta depende de la base de datos y la intercalación. En SQL
Server, Contains se asigna a SQL LIKE, que distingue entre mayúsculas y minúsculas. En SQLite, con la
intercalación predeterminada, se distingue entre mayúsculas y minúsculas.
Navegue a /Movies/Index . Anexe una cadena de consulta como ?searchString=Ghost a la dirección URL. Se
muestran las películas filtradas.

Si se cambia la firma del método Index para que tenga un parámetro con el nombre id , el parámetro id
coincidirá con el marcador {id} opcional para el conjunto de rutas predeterminado en Startup.cs.

app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});

Puede cambiar fácilmente el nombre del parámetro searchString por id con el comando rename. Haga clic con
el botón derecho en searchString > Cambiar nombre.
Se resaltarán los objetivos del cambio de nombre.

Cambie el parámetro por id y todas las apariciones de searchString se modificarán por id .

El método Index anterior:


public async Task<IActionResult> Index(string searchString)
{
var movies = from m in _context.Movie
select m;

if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}

return View(await movies.ToListAsync());


}

El método Index actualizado con el parámetro id :

public async Task<IActionResult> Index(string id)


{
var movies = from m in _context.Movie
select m;

if (!String.IsNullOrEmpty(id))
{
movies = movies.Where(s => s.Title.Contains(id));
}

return View(await movies.ToListAsync());


}

Ahora puede pasar el título de la búsqueda como datos de ruta (un segmento de dirección URL ) en lugar de como
un valor de cadena de consulta.

Sin embargo, no se puede esperar que los usuarios modifiquen la dirección URL cada vez que quieran buscar una
película. Por tanto, ahora deberá agregar elementos de la interfaz de usuario con los que podrán filtrar las
películas. Si cambió la firma del método Index para probar cómo pasar el parámetro ID enlazado a una ruta,
vuelva a cambiarlo para que tome un parámetro denominado searchString :
public async Task<IActionResult> Index(string searchString)
{
var movies = from m in _context.Movie
select m;

if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}

return View(await movies.ToListAsync());


}

Abra el archivo Views/Movies/Index.cshtml y agregue el marcado <form> resaltado a continuación:

ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
<a asp-action="Create">Create New</a>
</p>

<form asp-controller="Movies" asp-action="Index">


<p>
Title: <input type="text" name="SearchString">
<input type="submit" value="Filter" />
</p>
</form>

<table class="table">
<thead>

La etiqueta HTML <form> usa la aplicación auxiliar de etiquetas de formulario, por lo que cuando se envía el
formulario, la cadena de filtro se registra en la acción Index del controlador de películas. Guarde los cambios y
después pruebe el filtro.
No hay ninguna sobrecarga [HttpPost] del método Index como cabría esperar. No es necesario, porque el
método no cambia el estado de la aplicación, simplemente filtra los datos.
Después, puede agregar el método [HttpPost] Index siguiente.

[HttpPost]
public string Index(string searchString, bool notUsed)
{
return "From [HttpPost]Index: filter on " + searchString;
}

El parámetro notUsed se usa para crear una sobrecarga para el método Index . Hablaremos sobre esto más
adelante en el tutorial.
Si agrega este método, el invocador de acción coincidiría con el método [HttpPost] Index , mientras que el
método [HttpPost] Index se ejecutaría tal como se muestra en la imagen de abajo.

Sin embargo, aunque agregue esta versión de [HttpPost] al método Index , hay una limitación en cómo se ha
implementado todo esto. Supongamos que quiere marcar una búsqueda en particular o que quiere enviar un
vínculo a sus amigos donde puedan hacer clic para ver la misma lista filtrada de películas. Tenga en cuenta que la
dirección URL de la solicitud HTTP POST es la misma que la dirección URL de la solicitud GET
(localhost:xxxxx/Movies/Index): no hay información de búsqueda en la URL. La información de la cadena de
búsqueda se envía al servidor como un valor de campo de formulario. Puede comprobarlo con las herramientas
de desarrollo del explorador o con la excelente herramienta Fiddler. En la imagen de abajo se muestran las
herramientas de desarrollo del explorador Chrome:
Puede ver el parámetro de búsqueda y el token XSRF en el cuerpo de la solicitud. Tenga en cuenta, como se
mencionó en el tutorial anterior, que la aplicación auxiliar de etiquetas de formulario genera un token XSRF
antifalsificación. Como no se van a modificar datos, no es necesario validar el token con el método del controlador.
El parámetro de búsqueda se encuentra en el cuerpo de solicitud y no en la dirección URL. Por eso no se puede
capturar dicha información para marcarla o compartirla con otros usuarios. Para solucionar este problema,
especificaremos que la solicitud sea HTTP GET .
Observe cómo IntelliSense le ayuda a actualizar el marcado.
Observe que la fuente es diferente en la etiqueta <form> . Esta fuente distinta indica que la etiqueta es compatible
con las aplicaciones auxiliares de etiquetas.

Ahora, cuando se envía una búsqueda, la URL contiene la cadena de consulta de búsqueda. La búsqueda también
será dirigida al método de acción HttpGet Index , aunque tenga un método HttpPost Index .

El marcado siguiente muestra el cambio en la etiqueta form :

<form asp-controller="Movies" asp-action="Index" method="get">

Agregar búsqueda por género


Agregue la clase MovieGenreViewModel siguiente a la carpeta Models:
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace MvcMovie.Models
{
public class MovieGenreViewModel
{
public List<Movie> movies;
public SelectList genres;
public string movieGenre { get; set; }
}
}

El modelo de vista de película y género contendrá:


Una lista de películas.
SelectList , que contiene la lista de géneros. Esto permitirá al usuario seleccionar un género de la lista.
movieGenre , que contiene el género seleccionado.

Reemplace el método Index en MoviesController.cs por el código siguiente:

// Requires using Microsoft.AspNetCore.Mvc.Rendering;


public async Task<IActionResult> Index(string movieGenre, string searchString)
{
// Use LINQ to get list of genres.
IQueryable<string> genreQuery = from m in _context.Movie
orderby m.Genre
select m.Genre;

var movies = from m in _context.Movie


select m;

if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}

if (!String.IsNullOrEmpty(movieGenre))
{
movies = movies.Where(x => x.Genre == movieGenre);
}

var movieGenreVM = new MovieGenreViewModel();


movieGenreVM.genres = new SelectList(await genreQuery.Distinct().ToListAsync());
movieGenreVM.movies = await movies.ToListAsync();

return View(movieGenreVM);
}

El código siguiente es una consulta LINQ que recupera todos los géneros de la base de datos.

// Use LINQ to get list of genres.


IQueryable<string> genreQuery = from m in _context.Movie
orderby m.Genre
select m.Genre;

La SelectList de géneros se crea mediante la proyección de los distintos géneros (no queremos que nuestra lista
de selección tenga géneros duplicados).
movieGenreVM.genres = new SelectList(await genreQuery.Distinct().ToListAsync())

Agregar búsqueda por género a la vista de índice


Actualice Index.cshtml de la siguiente manera:
@model MvcMovie.Models.MovieGenreViewModel

@{
ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
<a asp-action="Create">Create New</a>
</p>

<form asp-controller="Movies" asp-action="Index" method="get">


<p>
<select asp-for="movieGenre" asp-items="Model.genres">
<option value="">All</option>
</select>

Title: <input type="text" name="SearchString">


<input type="submit" value="Filter" />
</p>
</form>

<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.movies[0].Title)
</th>
<th>
@Html.DisplayNameFor(model => model.movies[0].ReleaseDate)
</th>
<th>
@Html.DisplayNameFor(model => model.movies[0].Genre)
</th>
<th>
@Html.DisplayNameFor(model => model.movies[0].Price)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.movies)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.ReleaseDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Genre)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
<a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-action="Details" asp-route-id="@item.ID">Details</a> |
<a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
Examine la expresión lambda usada en la siguiente aplicación auxiliar HTML:
@Html.DisplayNameFor(model => model.movies[0].Title)

En el código anterior, la aplicación auxiliar HTML DisplayNameFor inspecciona la propiedad Title a la que se
hace referencia en la expresión lambda para determinar el nombre para mostrar. Puesto que la expresión lambda
se inspecciona en lugar de evaluarse, no recibirá una infracción de acceso cuando model , model.movies o
model.movies[0] sean null o estén vacíos. Cuando se evalúa la expresión lambda (por ejemplo,
@Html.DisplayFor(modelItem => item.Title) ), se evalúan los valores de propiedad del modelo.

Pruebe la aplicación haciendo búsquedas por género, título de la película y ambos.

A N T E R IO R S IG U IE N T E
Agregar un nuevo campo a una aplicación ASP.NET
Core
25/06/2018 • 7 minutes to read • Edit Online

Por Rick Anderson


En esta sección se usa Migraciones de Entity Framework Code First para agregar un nuevo campo al modelo y
migrar ese cambio a la base de datos.
Cuando se usa EF Code First para crear una base de datos de forma automática, Code First agrega una tabla a la
base de datos para ayudar a saber si el esquema de la base de datos está sincronizado con las clases del modelo a
partir del que se ha generado. Si no está sincronizado, EF produce una excepción. Esto facilita la detección de
problemas de código o base de datos incoherentes.

Adición de una propiedad de clasificación al modelo Movie


Abra el archivo Models/Movie.cs y agregue una propiedad Rating :
[!code-csharp]
[!code-csharp]
Compile la aplicación (Ctrl + Mayús + B ).
Dado que ha agregado un nuevo campo a la clase Movie , también debe actualizar la lista blanca de enlaces para
que se incluya esta nueva propiedad. En MoviesController.cs, actualice el atributo [Bind] de los métodos de
acción Create y Edit para incluir la propiedad Rating :

[Bind("ID,Title,ReleaseDate,Genre,Price,Rating")]

También debe actualizar las plantillas de vista para mostrar, crear y editar la nueva propiedad Rating en la vista
del explorador.
Edite el archivo /Views/Movies/Index.cshtml y agregue un campo Rating :
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.movies[0].Title)
</th>
<th>
@Html.DisplayNameFor(model => model.movies[0].ReleaseDate)
</th>
<th>
@Html.DisplayNameFor(model => model.movies[0].Genre)
</th>
<th>
@Html.DisplayNameFor(model => model.movies[0].Price)
</th>
<th>
@Html.DisplayNameFor(model => model.movies[0].Rating)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.movies)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.ReleaseDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Genre)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
@Html.DisplayFor(modelItem => item.Rating)
</td>
<td>

Actualice /Views/Movies/Create.cshtml con un campo Rating . Puede copiar o pegar el elemento "form group"
anterior y permitir que IntelliSense le ayude a actualizar los campos. IntelliSense funciona con aplicaciones
auxiliares de etiquetas. Nota: En la versión RTM de Visual Studio 2017 debe instalar Servicios de lenguaje Razor
para Razor IntelliSense. Esto se resolverá en la próxima versión.
La aplicación no funcionará hasta que la base de datos se actualice para incluir el nuevo campo. Si la ejecuta
ahora, se producirá la siguiente SqlException :
SqlException: Invalid column name 'Rating'.

Este error aparece porque la clase del modelo Movie actualizado es diferente al esquema de la tabla Movie de la
base de datos existente. (No hay ninguna columna Rating en la tabla de la base de datos).
Este error se puede resolver de varias maneras:
1. Haga que Entity Framework quite de forma automática la base de datos y la vuelva a crear basándose en el
nuevo esquema de la clase del modelo. Este enfoque resulta muy conveniente al principio del ciclo de
desarrollo cuando se está realizando el desarrollo activo en una base de datos de prueba; permite
desarrollar rápidamente el esquema del modelo y la base de datos juntos. La desventaja es que se pierden
los datos existentes en la base de datos, así que no use este enfoque en una base de datos de producción.
Usar un inicializador para inicializar automáticamente una base de datos con datos de prueba suele ser una
manera productiva de desarrollar una aplicación.
2. Modifique explícitamente el esquema de la base de datos existente para que coincida con las clases del
modelo. La ventaja de este enfoque es que se conservan los datos. Puede realizar este cambio de forma
manual o mediante la creación de un script de cambio de base de datos.
3. Use Migraciones de Code First para actualizar el esquema de la base de datos.
Para este tutorial se usa Migraciones de Code First.
Actualice la clase SeedData para que proporcione un valor para la nueva columna. A continuación se muestra un
cambio de ejemplo, aunque es conveniente realizarlo con cada new Movie .

new Movie
{
Title = "When Harry Met Sally",
ReleaseDate = DateTime.Parse("1989-1-11"),
Genre = "Romantic Comedy",
Rating = "R",
Price = 7.99M
},

Compile la solución.
En el menú Herramientas, seleccione Administrador de paquetes NuGet > Consola del Administrador de
paquetes.
En PCM, escriba los siguientes comandos:

Add-Migration Rating
Update-Database

El comando Add-Migration indica el marco de trabajo de migración para examinar el modelo Movie actual con el
esquema de base de datos Movie actual y para crear el código con el que se migrará la base de datos al nuevo
modelo. El nombre "Rating" es arbitrario y se usa para asignar nombre al archivo de migración. Resulta útil
emplear un nombre descriptivo para el archivo de migración.
Si elimina todos los registros de la base de datos, el inicializador inicializa la base de datos e incluye el campo
Rating . Puede hacerlo con los vínculos de eliminación en el explorador o desde SSOX.

Ejecute la aplicación y compruebe que puede crear, editar o mostrar películas con un campo Rating . También
debe agregar el campo Rating a las plantillas de vista Edit , Details y Delete .

A N T E R IO R S IG U IE N T E
Adición de validación
25/06/2018 • 17 minutes to read • Edit Online

Por Rick Anderson


En esta sección se agregará lógica de validación al modelo Movie y se asegurará de que las reglas de validación
se aplican siempre que un usuario crea o edita una película.

Respetar el principio DRY


Uno de los principios de diseño de MVC es DRY ("Una vez y solo una"). ASP.NET MVC le anima a que
especifique la función o el comportamiento una sola vez y luego lo refleje en el resto de la aplicación. Esto reduce
la cantidad de código que necesita escribir y hace que el código que escribe sea menos propenso a errores, así
como más fácil probar y de mantener.
La compatibilidad de validación proporcionada por MVC y Entity Framework Core Code First es un buen ejemplo
del principio DRY. Puede especificar las reglas de validación mediante declaración en un lugar (en la clase del
modelo) y las reglas se aplican en toda la aplicación.

Adición de reglas de validación al modelo de película


Abra el archivo Movie.cs. DataAnnotations proporciona un conjunto integrado de atributos de validación que se
aplican mediante declaración a cualquier clase o propiedad. (También contiene atributos de formato como
DataType , que ayudan a aplicar formato y no proporcionan validación).

Actualice la clase Movie para aprovechar los atributos de validación integrados Required , StringLength ,
RegularExpression y Range .
[!code-csharp]
[!code-csharp]
Los atributos de validación especifican el comportamiento que quiere aplicar en las propiedades del modelo al
que se aplican. Los atributos Required y MinimumLength indican que una propiedad debe tener un valor, pero
nada evita que un usuario escriba espacios en blanco para satisfacer esta validación. El atributo
RegularExpression se usa para limitar los caracteres que se pueden escribir. En el código anterior, Genre y
Rating solamente pueden usar letras (no se permiten espacios en blanco, números ni caracteres especiales). El
atributo Range restringe un valor a un intervalo determinado. El atributo StringLength permite establecer la
longitud máxima de una propiedad de cadena y, opcionalmente, su longitud mínima. Los tipos de valor (como
decimal , int , float , DateTime ) son intrínsecamente necesarios y no necesitan el atributo [Required] .

Cuando ASP.NET aplica automáticamente reglas de validación, logramos que la aplicación sea más sólida.
También nos permite asegurarnos de que todo se valida y que no nos dejamos ningún dato incorrecto en la base
de datos accidentalmente.

Interfaz de usuario de error de validación en MVC


Ejecute la aplicación y navegue al controlador Movies.
Pulse el vínculo Crear nueva para agregar una nueva película. Rellene el formulario con algunos valores no
válidos. En cuanto la validación del lado cliente de jQuery detecta el problema, muestra un mensaje de error.
NOTE
Es posible que no pueda escribir comas decimales en el campo Price . Para que la validación de jQuery sea compatible con
configuraciones regionales distintas del inglés que usan una coma (",") en lugar de un punto decimal y formatos de fecha
distintos del de Estados Unidos, debe seguir unos pasos para globalizar la aplicación. Consulte el problema 4076 de GitHub
para obtener instrucciones sobre cómo agregar la coma decimal.

Observe cómo el formulario presenta automáticamente un mensaje de error de validación adecuado en cada
campo que contiene un valor no válido. Los errores se aplican en el lado cliente (con JavaScript y jQuery) y en el
lado servidor (cuando un usuario tiene JavaScript deshabilitado).
Una ventaja importante es que no fue necesario cambiar ni una sola línea de código en la clase MoviesController
o en la vista Create.cshtml para habilitar esta interfaz de usuario de validación. El controlador y las vistas que creó
en pasos anteriores de este tutorial seleccionaron automáticamente las reglas de validación que especificó
mediante atributos de validación en las propiedades de la clase del modelo Movie . Pruebe la aplicación mediante
el método de acción Edit y se aplicará la misma validación.
Los datos del formulario no se enviarán al servidor hasta que dejen de producirse errores de validación de cliente.
Puede comprobarlo colocando un punto de interrupción en el método HTTP Post mediante la herramienta
Fiddler o las herramientas de desarrollo F12.

Cómo funciona la validación


Tal vez se pregunte cómo se generó la validación de la interfaz de usuario sin actualizar el código en el
controlador o las vistas. En el código siguiente se muestran los dos métodos Create .

// GET: Movies/Create
public IActionResult Create()
{
return View();
}

// POST: Movies/Create
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(
[Bind("ID,Title,ReleaseDate,Genre,Price, Rating")] Movie movie)
{
if (ModelState.IsValid)
{
_context.Add(movie);
await _context.SaveChangesAsync();
return RedirectToAction("Index");
}
return View(movie);
}

El primer método de acción Create (HTTP GET) muestra el formulario de creación inicial. La segunda versión (
[HttpPost] ) controla el envío de formulario. El segundo método Create (la versión [HttpPost] ) llama a
ModelState.IsValid para comprobar si la película tiene errores de validación. Al llamar a este método se evalúan
todos los atributos de validación que se hayan aplicado al objeto. Si el objeto tiene errores de validación, el
método Create vuelve a mostrar el formulario. Si no hay ningún error, el método guarda la nueva película en la
base de datos. En nuestro ejemplo de película, el formulario no se publica en el servidor si se detectan errores de
validación del lado cliente; cuando hay errores de validación en el lado cliente, no se llama nunca al segundo
método Create . Si deshabilita JavaScript en el explorador, se deshabilita también la validación del cliente y puede
probar si el método Create HTTP POST ModelState.IsValid detecta errores de validación.
Puede establecer un punto de interrupción en el método [HttpPost] Create y comprobar si nunca se llama al
método. La validación del lado cliente no enviará los datos del formulario si se detectan errores de validación. Si
deshabilita JavaScript en el explorador y después envía el formulario con errores, se alcanzará el punto de
interrupción. Puede seguir obteniendo validación completa sin JavaScript.
En la siguiente imagen se muestra cómo deshabilitar JavaScript en el explorador Firefox.
En la siguiente imagen se muestra cómo deshabilitar JavaScript en el explorador Chrome.

Después de deshabilitar JavaScript, publique los datos no válidos y siga los pasos del depurador.
Abajo se muestra una parte de la plantilla de vista Create.cshtml a la que se aplicó scaffolding en un paso anterior
de este tutorial. Los métodos de acción que se muestran arriba la usan para mostrar el formulario inicial y para
volver a mostrarlo en caso de error.

<form asp-action="Create">
<div class="form-horizontal">
<h4>Movie</h4>
<hr />

<div asp-validation-summary="ModelOnly" class="text-danger"></div>


<div class="form-group">
<label asp-for="Title" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Title" class="form-control" />
<span asp-validation-for="Title" class="text-danger"></span>
</div>
</div>

@*Markup removed for brevity.*@


</div>
</form>

La aplicación auxiliar de etiquetas de entrada usa los atributos DataAnnotations y genera los atributos HTML
necesarios para la validación de jQuery en el lado cliente. La aplicación auxiliar de etiquetas de validación muestra
errores de validación. Para más información, vea Introduction to model validation in ASP.NET Core MVC
(Introducción a la validación de modelos en ASP.NET Core MVC ).
Lo realmente bueno de este enfoque es que ni el controlador ni la plantilla de vista Create saben que las reglas
de validación actuales se están aplicando ni conocen los mensajes de error específicos que se muestran. Las reglas
de validación y las cadenas de error solo se especifican en la clase Movie . Estas mismas reglas de validación se
aplican automáticamente a la vista Edit y a cualquier otra vista de plantillas creada que edite el modelo.
Cuando necesite cambiar la lógica de validación, puede hacerlo exactamente en un solo lugar mediante la adición
de atributos de validación al modelo (en este ejemplo, la clase Movie ). No tendrá que preocuparse de que
diferentes partes de la aplicación sean incoherentes con el modo en que se aplican las reglas: toda la lógica de
validación se definirá en un solo lugar y se usará en todas partes. Esto mantiene el código muy limpio y hace que
sea fácil de mantener y evolucionar. También significa que respeta totalmente el principio DRY.

Uso de atributos DataType


Abra el archivo Movie.cs y examine la clase Movie . El espacio de nombres System.ComponentModel.DataAnnotations
proporciona atributos de formato además del conjunto integrado de atributos de validación. Ya hemos aplicado
un valor de enumeración DataType en la fecha de lanzamiento y los campos de precio. En el código siguiente se
muestran las propiedades ReleaseDate y Price con el atributo DataType adecuado.

[Display(Name = "Release Date")]


[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }

[Range(1, 100)]
[DataType(DataType.Currency)]
public decimal Price { get; set; }

Los atributos DataType solo proporcionan sugerencias para que el motor de vista aplique formato a los datos (y
ofrece atributos o elementos como <a> para las direcciones URL y <a href="mailto:EmailAddress.com"> para el
correo electrónico). Use el atributo RegularExpression para validar el formato de los datos. El atributo DataType
no es un atributo de validación, sino que se usa para especificar un tipo de datos más específico que el tipo
intrínseco de la base de datos. En este caso solo queremos realizar un seguimiento de la fecha, no la hora. La
enumeración DataType proporciona muchos tipos de datos, como Date (Fecha), Time (Hora), PhoneNumber
(Número de teléfono), Currency (Moneda), EmailAddress (Dirección de correo electrónico), etc. El atributo
DataType también puede permitir que la aplicación proporcione automáticamente características específicas del
tipo. Por ejemplo, se puede crear un vínculo mailto: para DataType.EmailAddress y se puede proporcionar un
selector de datos para DataType.Date en exploradores compatibles con HTML5. Los atributos DataType emiten
atributos HTML 5 data- (se pronuncia "datos dash") que los exploradores HTML 5 pueden comprender. Los
atributos DataType no proporcionan ninguna validación.
DataType.Date no especifica el formato de la fecha que se muestra. De manera predeterminada, el campo de
datos se muestra según los formatos predeterminados basados en el elemento CultureInfo del servidor.
El atributo DisplayFormat se usa para especificar el formato de fecha de forma explícita:

[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]


public DateTime ReleaseDate { get; set; }

El valor ApplyFormatInEditMode especifica que el formato se debe aplicar también cuando el valor se muestra en
un cuadro de texto para su edición. En algunos campos este comportamiento puede no ser conveniente. Por
poner un ejemplo, es probable que con valores de moneda no se quiera que el símbolo de la divisa se incluya en
el cuadro de texto editable.
El atributo DisplayFormat puede usarse por sí solo, pero normalmente se recomienda usar el atributo DataType .
El atributo DataType transmite la semántica de los datos en contraposición a cómo se representa en una pantalla
y ofrece las siguientes ventajas que no proporciona DisplayFormat:
El explorador puede habilitar características de HTML5 (por ejemplo, mostrar un control de calendario, el
símbolo de moneda adecuado según la configuración regional, vínculos de correo electrónico, etc.).
De manera predeterminada, el explorador representa los datos con el formato correcto según la
configuración regional.
El atributo DataType puede habilitar MVC para que elija la plantilla de campo adecuada para representar
los datos ( DisplayFormat , si se usa por sí solo, usa la plantilla de cadena).

NOTE
La validación de jQuery no funciona con el atributo Range ni DateTime . Por ejemplo, el código siguiente siempre muestra
un error de validación del lado cliente, incluso cuando la fecha está en el intervalo especificado:
[Range(typeof(DateTime), "1/1/1966", "1/1/2020")]

Debe deshabilitar la validación de fechas de jQuery para usar el atributo Range con DateTime . Por lo general no
se recomienda compilar fechas fijas en los modelos, así que desaconseja usar el atributo Range y DateTime .
El código siguiente muestra la combinación de atributos en una línea:
[!code-csharp]
[!code-csharp]
En la siguiente parte de la serie de tutoriales, revisaremos la aplicación y realizaremos algunas mejoras a los
métodos Details y Delete generados automáticamente.

Recursos adicionales
Trabajar con formularios
Globalización y localización
Introducción a las aplicaciones auxiliares de etiquetas
Creación de aplicaciones auxiliares de etiquetas

A N T E R IO R S IG U IE N T E
Examinar los métodos Details y Delete de una
aplicación ASP.NET Core
25/06/2018 • 5 minutes to read • Edit Online

Por Rick Anderson


Abra el controlador Movie y examine el método Details :
[!code-csharp]
[!code-csharp]
El motor de scaffolding de MVC que creó este método de acción agrega un comentario en el que se muestra una
solicitud HTTP que invoca el método. En este caso se trata de una solicitud GET con tres segmentos de dirección
URL, el controlador Movies , el método Details y un valor id . Recuerde que estos segmentos se definen en
Startup.cs.

app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});

EF facilita el proceso de búsqueda de datos mediante el método SingleOrDefaultAsync . Una característica de


seguridad importante integrada en el método es que el código comprueba que el método de búsqueda haya
encontrado una película antes de intentar hacer nada con ella. Por ejemplo, un pirata informático podría introducir
errores en el sitio cambiando la dirección URL creada por los vínculos de http://localhost:xxxx/Movies/Details/1 a
algo parecido a http://localhost:xxxx/Movies/Details/12345 (o algún otro valor que no represente una película
real). Si no comprobara una película null, la aplicación generaría una excepción.
Examine los métodos Delete y DeleteConfirmed .
[!code-csharp]
[!code-csharp]
Tenga en cuenta que el método HTTP GET Delete no elimina la película especificada, sino que devuelve una vista de
la película donde puede enviar (HttpPost) la eliminación. La acción de efectuar una operación de eliminación en
respuesta a una solicitud GET (o con este propósito efectuar una operación de edición, creación o cualquier otra
operación que modifique los datos) genera una vulnerabilidad de seguridad.
El método [HttpPost] que elimina los datos se denomina DeleteConfirmed para proporcionar al método HTTP
POST una firma o nombre únicos. Las dos firmas de método se muestran a continuación:

// GET: Movies/Delete/5
public async Task<IActionResult> Delete(int? id)
{
// POST: Movies/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{

Common Language Runtime (CLR ) requiere métodos sobrecargados para disponer de una firma de parámetro
única (mismo nombre de método, pero lista de parámetros diferente). En cambio, aquí necesita dos métodos
Delete (uno para GET y otro para POST ) que tienen la misma firma de parámetro (ambos deben aceptar un
número entero como parámetro).
Hay dos enfoques para este problema. Uno consiste en proporcionar nombres diferentes a los métodos, que es lo
que hizo el mecanismo de scaffolding en el ejemplo anterior. Pero esto implica un pequeño problema: ASP.NET
asigna segmentos de una dirección URL a los métodos de acción por nombre y, si cambia el nombre de un método,
normalmente el enrutamiento no podría encontrar ese método. La solución es la que ve en el ejemplo, que consiste
en agregar el atributo ActionName("Delete") al método DeleteConfirmed . Ese atributo efectúa la asignación para el
sistema de enrutamiento para que una dirección URL que incluya /Delete/ para una solicitud POST busque el
método DeleteConfirmed .
Otra solución alternativa común para los métodos que tienen nombres y firmas idénticos consiste en cambiar la
firma del método POST artificialmente para incluir un parámetro adicional (sin usar). Es lo que hicimos en una
publicación anterior, cuando agregamos el parámetro notUsed . Podría hacer lo mismo aquí para el método
[HttpPost] Delete :

// POST: Movies/Delete/6
[ValidateAntiForgeryToken]
public async Task<IActionResult> Delete(int id, bool notUsed)

Publicar en Azure
Vea Publicar una aplicación web de ASP.NET Core en Azure App Service con Visual Studio para obtener
instrucciones sobre cómo publicar esta aplicación en Azure con Visual Studio. También se puede publicar la
aplicación desde la línea de comandos.

A N T E R IO R
Compilación de API web con ASP.NET Core
21/06/2018 • 6 minutes to read • Edit Online

Por Scott Addie


Vea o descargue el código de ejemplo (cómo descargarlo)
En este documento se explica cómo crear una API web en ASP.NET Core y los casos en los que se recomienda usar
cada una.

Derivación de una clase desde ControllerBase


Herede desde la clase ControllerBase en un controlador diseñado para funcionar como API web. Por ejemplo:
[!code-csharp]
[!code-csharp]
La clase ControllerBase proporciona acceso a varios métodos y propiedades. En el ejemplo anterior, algunos
métodos de este tipo incluyen BadRequest y CreatedAtAction. Estos métodos se invocan en los métodos de acción
para devolver los códigos de estado HTTP 400 y 201, respectivamente. La propiedad ModelState, que también se
proporciona con ControllerBase , se usa para realizar la validación del modelo de solicitud.

Anotación de una clase con ApiControllerAttribute


ASP.NET Core 2.1 incorpora el atributo [ApiController] para designar una clase de controlador de API web. Por
ejemplo:
[!code-csharp]
Este atributo se suele emparejar con ControllerBase para acceder a propiedades y métodos útiles. ControllerBase
proporciona acceso a métodos como NotFound y File.
Otro enfoque consiste en crear una clase de controlador base personalizada anotada con el atributo
[ApiController] :

[!code-csharp]
En las secciones siguientes se describen las ventajas de las características que aporta el atributo.
Respuestas HTTP 400 automáticas
Los errores de validación desencadenan automáticamente una respuesta HTTP 400. El código siguiente deja de ser
necesario en las acciones:
[!code-csharp]
Este comportamiento predeterminado se deshabilita con siguiente código en Startup.ConfigureServices:
[!code-csharp]
Inferencia de parámetro de origen de enlace
Un atributo de origen de enlace define la ubicación del valor del parámetro de una acción. Existen los atributos de
origen de enlace siguientes:
ATRIBUTO ORIGEN DE ENLACE

[FromBody] Cuerpo de la solicitud

[FromForm] Datos del formulario en el cuerpo de la solicitud

[FromHeader] Encabezado de la solicitud

[FromQuery] Parámetro de la cadena de consulta de la solicitud

[FromRoute] Datos de ruta de la solicitud actual

[FromServices] Servicio de solicitud insertado como parámetro de acción

NOTE
No use [FromRoute] si los valores pueden contener %2f (es decir, / ) porque %2f no incluirá el carácter sin escape / .
Use [FromQuery] si el valor puede contener %2f .

Sin el [ApiController] atributo, los atributos de origen de enlace se definen explícitamente. En el ejemplo
siguiente, el atributo [FromQuery] indica que el valor del parámetro discontinuedOnly se proporciona en la cadena
de consulta de la dirección URL de la solicitud:
[!code-csharp]
Las reglas de inferencia se aplican para los orígenes de datos predeterminados de los parámetros de acción. Estas
reglas configuran los orígenes de enlace que aplicaría manualmente a los parámetros de acción. Los atributos de
origen de enlace presentan este comportamiento:
[FromBody] se infiere para los parámetros de tipo complejo. La excepción a esta regla es cualquier tipo
integrado complejo que tenga un significado especial, como IFormCollection o CancellationToken. El código de
inferencia del origen de enlace omite esos tipos especiales. Cuando una acción contiene más de un parámetro
que se especifica explícitamente (a través de [FromBody] ) o se infiere como enlazado desde el cuerpo de la
solicitud, se produce una excepción. Por ejemplo, las firmas de acción siguientes provocan una excepción:
[!code-csharp]
[FromForm ] se infiere para los parámetros de acción de tipo IFormFile o IFormFileCollection. No se infiere
para los tipos simples o definidos por el usuario.
[FromRoute] se infiere para cualquier nombre de parámetro de acción que coincida con un parámetro de la
plantilla de ruta. Si varias rutas coinciden con un parámetro de acción, cualquier valor de ruta se considera
[FromRoute] .
[FromQuery] se infiere para cualquier otro parámetro de acción.

Las reglas de inferencia predeterminadas se deshabilitan con el código siguiente en Startup.ConfigureServices:


[!code-csharp]
Inferencia de solicitud de varios elementos o datos de formulario
Si un parámetro de acción se anota con el atributo [FromForm], se infiere el tipo de contenido de la solicitud
multipart/form-data .

El comportamiento predeterminado se deshabilita con el siguiente código en Startup.ConfigureServices:


[!code-csharp]
Requisito de enrutamiento mediante atributos
El enrutamiento mediante atributos pasa a ser un requisito. Por ejemplo:
[!code-csharp]
Las acciones dejan de estar disponibles a través de las rutas convencionales definidas en UseMvc o mediante
UseMvcWithDefaultRoute en Startup.Configure.

Recursos adicionales
Tipos de valor devuelto de acción del controlador
Formateadores personalizados
Aplicación de formato a datos de respuesta
Páginas de ayuda mediante Swagger
Enrutamiento a acciones del controlador
Crear una API web con ASP.NET Core y Visual Studio
Code
17/05/2018 • 25 minutes to read • Edit Online

Por Rick Anderson y Mike Wasson


En este tutorial se compila una API web para administrar una lista de tareas pendientes. No se construye una
interfaz de usuario.
Hay tres versiones de este tutorial:
macOS, Linux y Windows: API Web con Visual Studio Code (este tutorial)
macOS: API Web con Visual Studio para Mac
Windows: API Web con Visual Studio para Windows

Información general
En este tutorial se crea la siguiente API:

API DESCRIPTION CUERPO DE LA SOLICITUD CUERPO DE LA RESPUESTA

GET /api/todo Obtener todas las tareas Ninguna Matriz de tareas pendientes
pendientes

GET /api/todo/{id} Obtener un elemento por Ninguna Tarea pendiente


identificador

POST /api/todo Agregar un nuevo elemento Tarea pendiente Tarea pendiente

PUT /api/todo/{id} Actualizar un elemento Tarea pendiente Ninguna


existente

DELETE /api/todo/{id} Eliminar un elemento Ninguna Ninguna

El siguiente diagrama muestra el diseño básico de la aplicación.

El cliente es todo lo que consume la API web (aplicación móvil, explorador, etcétera). En este tutorial no se
crea ningún cliente. Postman o curl se utilizan como el cliente para probar la aplicación.
Un modelo es un objeto que representa los datos de la aplicación. En este caso, el único modelo es una tarea
pendiente. Los modelos se representan como clases de C#, también conocidas como clases POCO (del
inglés Plain Old C# Object, objetos CRL antiguos sin formato).
Un controlador es un objeto que controla solicitudes HTTP y crea la respuesta HTTP. Esta aplicación tiene un
único controlador.
Para simplificar el tutorial, la aplicación no usa una base de datos persistente. La aplicación de ejemplo
almacena tareas pendientes en una base de datos en memoria.

Requisitos previos
Install the following:
.NET Core SDK 2.0 or later
Visual Studio Code
C# for Visual Studio Code
.NET Core 2.1 SDK or later
Visual Studio Code
C# for Visual Studio Code

Crear el proyecto
Desde una consola, ejecute los siguientes comandos:

dotnet new webapi -o TodoApi


code TodoApi

La carpeta TodoApi se abre en Visual Studio Code (VS Code). Seleccione el archivo Startup.cs.
Seleccione Sí en el mensaje de advertencia "Required assets to build and debug are missing from 'TodoApi'.
Add them?" (Faltan los activos necesarios para compilar y depurar en 'TodoApi'. ¿Quiere agregarlos?).
Seleccione Restaurar en el mensaje de información "There are unresolved dependencies" (Hay dependencias
no resueltas).
Presione Depurar (F5) para compilar y ejecutar el programa. En un navegador, vaya a
http://localhost:5000/api/values. Se muestra el siguiente resultado:

["value1","value2"]

Vea Ayuda de Visual Studio Code para obtener sugerencias sobre el uso de VS Code.

Agregar compatibilidad con Entity Framework Core


Al crear un proyecto en ASP.NET Core 2.0, se agrega la referencia de paquete Microsoft.AspNetCore.All al archivo
TodoApi.csproj:
[!code-xml]
Al crear un proyecto en ASP.NET Core 2.1 o posterior, se agrega la referencia de paquete
Microsoft.AspNetCore.App al archivo TodoApi.csproj:
[!code-xml]
No es necesario instalar el proveedor de base de datos Entity Framework Core InMemory por separado. Este
proveedor de base de datos permite usar Entity Framework Core con una base de datos en memoria.

Agregar una clase de modelo


Un modelo es un objeto que representa los datos de la aplicación. En este caso, el único modelo es una tarea
pendiente.
Agregue una carpeta denominada Models. Puede colocar clases de modelo en cualquier lugar del proyecto, pero la
carpeta Models se usa por convención.
Agregue una clase TodoItem con el siguiente código:
namespace TodoApi.Models
{
public class TodoItem
{
public long Id { get; set; }
public string Name { get; set; }
public bool IsComplete { get; set; }
}
}

La base de datos genera el Id cuando se crea TodoItem .

Crear el contexto de base de datos


El contexto de base de datos es la clase principal que coordina la funcionalidad de Entity Framework para un
modelo de datos determinado. Esta clase se crea al derivar de la clase Microsoft.EntityFrameworkCore.DbContext .
Agregue una clase TodoContext a la carpeta Models:

using Microsoft.EntityFrameworkCore;

namespace TodoApi.Models
{
public class TodoContext : DbContext
{
public TodoContext(DbContextOptions<TodoContext> options)
: base(options)
{
}

public DbSet<TodoItem> TodoItems { get; set; }


}
}

Registrar el contexto de base de datos


En este paso, el contexto de base de datos se registra con el contenedor de inserción de dependencias. Los servicios
(por ejemplo, el contexto de la base de datos) que se registran con el contenedor de inserción de dependencias (DI)
están disponibles para los controladores.
Registre el contexto de la base de datos con el contenedor de servicio mediante la compatibilidad integrada para
inserción de dependencias. Reemplace el contenido del archivo Startup.cs con el código siguiente:
[!code-csharp]
[!code-csharp]
El código anterior:
Quita el código no usado.
Especifica que se inserte una base de datos en memoria en el contenedor de servicios.

Adición de un controlador
En la carpeta Controladores, cree una clase denominada TodoController . Reemplace el contenido por el siguiente
código:
[!code-csharp]
El código anterior define una clase de controlador de API sin métodos. En las secciones siguientes, se agregan
métodos para implementar la API.
[!code-csharp]
El código anterior define una clase de controlador de API sin métodos. En las secciones siguientes, se agregan
métodos para implementar la API. La clase se anota con un atributo [ApiController] para habilitar algunas
características muy prácticas. Para más información sobre las características que el atributo habilita, vea Anotación
de una clase con ApiControllerAttribute.
El constructor del controlador usa la inserción de dependencias para insertar el contexto de base de datos (
TodoContext ) en el controlador. El contexto de base de datos se usa en cada uno de los métodos CRUD del
controlador. El constructor agrega un elemento a la base de datos en memoria si no existe ninguno.

Tareas pendientes
Para obtener tareas pendientes, agregue estos métodos a la clase TodoController :
[!code-csharp]
[!code-csharp]
Estos métodos implementan los dos métodos GET:
GET /api/todo
GET /api/todo/{id}

Esta es una respuesta HTTP de ejemplo del método GetAll :

[
{
"id": 1,
"name": "Item1",
"isComplete": false
}
]

Más adelante en el tutorial, veremos cómo se puede ver la respuesta HTTP por medio de Postman o curl.
Enrutamiento y rutas URL
El atributo [HttpGet] indica un método que responde a una solicitud HTTP GET. La ruta de dirección URL para
cada método se construye como sigue:
Tome la cadena de plantilla en el atributo Route del controlador:
[!code-csharp]
[!code-csharp]
Reemplace [controller] por el nombre del controlador, que es el nombre de clase de controlador sin el sufijo
"Controller". En este ejemplo, el nombre de clase de controlador es TodoController y el nombre de raíz es
"todo". El enrutamiento en ASP.NET Core no distingue entre mayúsculas y minúsculas.
Si el atributo [HttpGet] tiene una plantilla de ruta (como [HttpGet("/products")] ), anexiónela a la ruta de
acceso. En este ejemplo no se usa una plantilla. Para más información, vea Enrutamiento mediante atributos con
atributos Http[Verb].
En el siguiente método GetById , "{id}" es una variable de marcador de posición correspondiente al identificador
único de la tarea pendiente. Cuando GetById se invoca, asigna el valor "{id}" de la dirección URL al parámetro
id del método.
[!code-csharp]
[!code-csharp]
Name = "GetTodo" crea una ruta con nombre. Rutas con nombre:
Permita que la aplicación cree un vínculo HTTP mediante el nombre de ruta.
Se explican más adelante en el tutorial.
Valores devueltos
El método GetAll devuelve una colección de objetos TodoItem . MVC serializa automáticamente el objeto a JSON
y escribe el JSON en el cuerpo del mensaje de respuesta. El código de respuesta para este método es 200,
suponiendo que no haya ninguna excepción no controlada. Las excepciones no controladas se convierten en
errores 5xx.
En cambio, el método GetById devuelve el tipo más general IActionResult, que representa una amplia gama de
tipos de valor devuelto. GetById tiene dos tipos de valor devueltos distintos:
Si no hay ningún elemento que coincida con el identificador solicitado, el método devuelve un error 404.
Devolver NotFound genera una respuesta HTTP 404.
En caso contrario, el método devuelve 200 con un cuerpo de respuesta JSON. Devolver Ok genera una
respuesta HTTP 200.
En cambio, el método GetById devuelve el tipo ActionResult<T>, que representa una amplia gama de tipos de
valor devuelto. GetById tiene dos tipos de valor devueltos distintos:
Si no hay ningún elemento que coincida con el identificador solicitado, el método devuelve un error 404.
Devolver NotFound genera una respuesta HTTP 404.
En caso contrario, el método devuelve 200 con un cuerpo de respuesta JSON. Devolver item genera una
respuesta HTTP 200.
Iniciar la aplicación
En VS Code, presione F5 para iniciar la aplicación. Vaya a http://localhost:5000/api/todo (el controlador Todo que
se acaba de crear).

Llamar a Web API con jQuery


En esta sección, se agrega una página HTML que usa jQuery para llamar a Web API. jQuery inicia la solicitud y
actualiza la página con los detalles de la respuesta de la API.
Configure el proyecto para atender archivos estáticos y para permitir la asignación de archivos predeterminada.
Esto se logra invocando los métodos de extensión UseStaticFiles y UseDefaultFiles en Startup.Configure. Para
obtener más información, consulte Archivos estáticos.

public void Configure(IApplicationBuilder app)


{
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseMvc();
}

Agregue un archivo HTML denominado index.html al directorio wwwroot del proyecto. Reemplace el contenido
por el siguiente marcado:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>To-do CRUD</title>
<style>
input[type='submit'], button, [aria-label] {
cursor: pointer;
}

#spoiler {
display: none;
}

table {
font-family: Arial, sans-serif;
border: 1px solid;
border-collapse: collapse;
}

th {
background-color: #0066CC;
color: white;
}

td {
border: 1px solid;
padding: 5px;
}
</style>
</head>
<body>
<h1>To-do CRUD</h1>
<h3>Add</h3>
<form action="javascript:void(0);" method="POST" onsubmit="addItem()">
<input type="text" id="add-name" placeholder="New to-do">
<input type="submit" value="Add">
</form>

<div id="spoiler">
<h3>Edit</h3>
<form class="my-form">
<input type="hidden" id="edit-id">
<input type="checkbox" id="edit-isComplete">
<input type="text" id="edit-name">
<input type="submit" value="Edit">
<a onclick="closeInput()" aria-label="Close">&#10006;</a>
</form>
</div>

<p id="counter"></p>

<table>
<tr>
<th>Is Complete</th>
<th>Name</th>
<th></th>
<th></th>
</tr>
<tbody id="todos"></tbody>
</table>

<script src="https://code.jquery.com/jquery-3.3.1.min.js"
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
crossorigin="anonymous"></script>
<script src="site.js"></script>
</body>
</html>
Agregue un archivo JavaScript denominado site.js al directorio wwwroot del proyecto. Reemplace el contenido por
el siguiente código:

const uri = 'api/todo';


let todos = null;
function getCount(data) {
const el = $('#counter');
let name = 'to-do';
if (data) {
if (data > 1) {
name = 'to-dos';
}
el.text(data + ' ' + name);
} else {
el.html('No ' + name);
}
}

$(document).ready(function () {
getData();
});

function getData() {
$.ajax({
type: 'GET',
url: uri,
success: function (data) {
$('#todos').empty();
getCount(data.length);
$.each(data, function (key, item) {
const checked = item.isComplete ? 'checked' : '';

$('<tr><td><input disabled="true" type="checkbox" ' + checked + '></td>' +


'<td>' + item.name + '</td>' +
'<td><button onclick="editItem(' + item.id + ')">Edit</button></td>' +
'<td><button onclick="deleteItem(' + item.id + ')">Delete</button></td>' +
'</tr>').appendTo($('#todos'));
});

todos = data;
}
});
}

function addItem() {
const item = {
'name': $('#add-name').val(),
'isComplete': false
};

$.ajax({
type: 'POST',
accepts: 'application/json',
url: uri,
contentType: 'application/json',
data: JSON.stringify(item),
error: function (jqXHR, textStatus, errorThrown) {
alert('here');
},
success: function (result) {
getData();
$('#add-name').val('');
}
});
}

function deleteItem(id) {
$.ajax({
$.ajax({
url: uri + '/' + id,
type: 'DELETE',
success: function (result) {
getData();
}
});
}

function editItem(id) {
$.each(todos, function (key, item) {
if (item.id === id) {
$('#edit-name').val(item.name);
$('#edit-id').val(item.id);
$('#edit-isComplete').val(item.isComplete);
}
});
$('#spoiler').css({ 'display': 'block' });
}

$('.my-form').on('submit', function () {
const item = {
'name': $('#edit-name').val(),
'isComplete': $('#edit-isComplete').is(':checked'),
'id': $('#edit-id').val()
};

$.ajax({
url: uri + '/' + $('#edit-id').val(),
type: 'PUT',
accepts: 'application/json',
contentType: 'application/json',
data: JSON.stringify(item),
success: function (result) {
getData();
}
});

closeInput();
return false;
});

function closeInput() {
$('#spoiler').css({ 'display': 'none' });
}

Puede que sea necesario realizar un cambio en la configuración de inicio del proyecto de ASP.NET Core para
comprobar la página HTML localmente. Abra launchSettings.json en el directorio Properties del proyecto. Quite la
propiedad launchUrl para forzar a la aplicación a abrirse en index.html, esto es, el archivo predeterminado del
proyecto.
Existen varias formas de obtener jQuery. En el fragmento de código anterior, la biblioteca se carga desde una red
CDN. Este ejemplo es un ejemplo de CRUD completo de llamada a la API de jQuery. Existen más características en
este ejemplo para hacer que la experiencia sea más completa. Aquí verá algunas explicaciones sobre las llamadas a
la API.
Obtener una lista de tareas pendientes
Para obtener una lista de tareas pendientes, envíe una solicitud HTTP GET a /api/todo.
La función de JQuery ajax envía una solicitud AJAX a la API, que devuelve código JSON que representa un objeto
o una matriz. Esta función puede controlar todas las formas de interacción de HTTP enviando una solicitud HTTP a
la url especificada. GET se emite como type . La función de devolución de llamada success se invoca si la
solicitud se realiza correctamente. En la devolución de llamada, el DOM se actualiza con la información de la tarea
pendiente.
$(document).ready(function () {
getData();
});

function getData() {
$.ajax({
type: 'GET',
url: uri,
success: function (data) {
$('#todos').empty();
getCount(data.length);
$.each(data, function (key, item) {
const checked = item.isComplete ? 'checked' : '';

$('<tr><td><input disabled="true" type="checkbox" ' + checked + '></td>' +


'<td>' + item.name + '</td>' +
'<td><button onclick="editItem(' + item.id + ')">Edit</button></td>' +
'<td><button onclick="deleteItem(' + item.id + ')">Delete</button></td>' +
'</tr>').appendTo($('#todos'));
});

todos = data;
}
});
}

Agregar una tarea pendiente


Para agregar una tarea pendiente, envíe una solicitud HTTP POST a /api/todo. El cuerpo de la solicitud debe
contener un objeto de tarea pendiente. La función ajax usa POST para llamar a la API. En las solicitudes POST y
PUT , el cuerpo de la solicitud representa los datos enviados a la API. La API espera un cuerpo de solicitud con
formato JSON. Las opciones accepts y contentType se establecen en application/json para clasificar el tipo de
medio que se va a recibir y a enviar respectivamente. Los datos se convierten en un objeto JSON usando
JSON.stringify . Cuando la API devuelve un código de estado correcto, se invoca la función getData para
actualizar la tabla HTML.

function addItem() {
const item = {
'name': $('#add-name').val(),
'isComplete': false
};

$.ajax({
type: 'POST',
accepts: 'application/json',
url: uri,
contentType: 'application/json',
data: JSON.stringify(item),
error: function (jqXHR, textStatus, errorThrown) {
alert('here');
},
success: function (result) {
getData();
$('#add-name').val('');
}
});
}

Actualizar una tarea pendiente


Actualizar una tarea pendiente es muy parecido a agregarla, ya que ambos procesos se basan en el cuerpo de
solicitud. En este caso, la única diferencia real entre ambos es que url cambia para reflejar el identificador único
del elemento, y type es PUT .

$.ajax({
url: uri + '/' + $('#edit-id').val(),
type: 'PUT',
accepts: 'application/json',
contentType: 'application/json',
data: JSON.stringify(item),
success: function (result) {
getData();
}
});

Eliminar una tarea pendiente


Para eliminar una tarea pendiente, hay que establecer type de la llamada de AJAX en DELETE y especificar el
identificador único de la tarea en la dirección URL.

$.ajax({
url: uri + '/' + id,
type: 'DELETE',
success: function (result) {
getData();
}
});

Implementar las otras operaciones CRUD


En las secciones siguientes, se agregan los métodos Create , Update y Delete al controlador.
Crear
Agregue el siguiente método Create :
[!code-csharp]
El código anterior es un método HTTP POST, según indica el atributo [HttpPost]. El atributo [FromBody] indica a
MVC que obtenga el valor de la tarea pendiente del cuerpo de la solicitud HTTP.
[!code-csharp]
El código anterior es un método HTTP POST, según indica el atributo [HttpPost]. MVC obtiene el valor de la tarea
pendiente del cuerpo de la solicitud HTTP.
El método CreatedAtRoute realiza las acciones siguientes:
Devuelve una respuesta 201. HTTP 201 es la respuesta estándar para un método HTTP POST que crea un
recurso en el servidor.
Agrega un encabezado de ubicación a la respuesta. El encabezado de ubicación especifica el URI de la tarea
pendiente recién creada. Vea 10.2.2 201 Created (10.2.2 201 creada).
Usa la ruta denominada "GetTodo" para crear la dirección URL. La ruta con nombre "GetTodo" se define en
GetById :

[!code-csharp]
[!code-csharp]
Usar Postman para enviar una solicitud de creación
Inicia la aplicación.
Abra Postman.
Actualice el número de puerto en la dirección URL de localhost.
Establezca el método HTTP en POST.
Haga clic en la pestaña Body (Cuerpo).
Seleccione el botón de radio Raw (Sin formato).
Establezca el tipo en JSON (application/json).
Escriba un cuerpo de solicitud con una tarea pendiente parecida al siguiente JSON:

{
"name":"walk dog",
"isComplete":true
}

Haga clic en el botón Send (Enviar).

TIP
Si no aparece ninguna respuesta tras hacer clic en Send (Enviar), deshabilite la opción SSL certification verification
(Comprobación de certificación SSL). La encontrará en File > Settings (Archivo > Configuración). Vuelva a hacer clic en el
botón Send (Enviar) después de deshabilitar la configuración.

Haga clic en la pestaña Headers (Encabezados) del panel Response (Respuesta) y copie el valor de encabezado de
Location (Ubicación):
El URI del encabezado de ubicación puede utilizarse para acceder al nuevo elemento.
Actualizar
Agregue el siguiente método Update :
[!code-csharp]
[!code-csharp]
Update es similar a Create , salvo por el hecho de que usa HTTP PUT. La respuesta es 204 Sin contenido. Según la
especificación HTTP, una solicitud PUT requiere que el cliente envíe toda la entidad actualizada, no solo los deltas.
Para admitir actualizaciones parciales, use HTTP PATCH.
Use Postman para actualizar el nombre de la tarea pendiente a "walk cat":
Eliminar
Agregue el siguiente método Delete :

[HttpDelete("{id}")]
public IActionResult Delete(long id)
{
var todo = _context.TodoItems.Find(id);
if (todo == null)
{
return NotFound();
}

_context.TodoItems.Remove(todo);
_context.SaveChanges();
return NoContent();
}

La respuesta de Delete es 204 Sin contenido.


Use Postman para eliminar la tarea pendiente:
Ayuda de Visual Studio Code
Introducción
Depuración
Terminal integrado
Métodos abreviados de teclado
Funciones rápidas de teclado de macOS
Métodos abreviados de teclado de Linux
Métodos abreviados de teclado de Windows

Pasos siguientes
Para más información sobre cómo usar una base de datos persistente, vea:
Creación de una aplicación web de páginas de Razor con ASP.NET Core
Trabajo con datos en ASP.NET Core
Páginas de ayuda de ASP.NET Core Web API mediante Swagger
Enrutamiento a acciones del controlador
Compilación de API web con ASP.NET Core
Tipos de valor devuelto de acción del controlador
Para obtener información sobre la implementación de una API, como en Azure App Service, vea la
documentación sobre Hospedaje e implementación.
Vea o descargue el código de ejemplo. Vea cómo descargarlo.
Crear una API web con ASP.NET Core y Visual Studio
para Mac
25/06/2018 • 25 minutes to read • Edit Online

Por Rick Anderson y Mike Wasson


En este tutorial se compila una API web para administrar una lista de tareas pendientes. No se construye la interfaz
de usuario.
Hay tres versiones de este tutorial:
macOS: API web con Visual Studio para Mac (este tutorial)
Windows: API web con Visual Studio para Windows
macOS, Linux y Windows: API web con Visual Studio Code

Información general
En este tutorial se crea la siguiente API:

API DESCRIPTION CUERPO DE LA SOLICITUD CUERPO DE LA RESPUESTA

GET /api/todo Obtener todas las tareas Ninguna Matriz de tareas pendientes
pendientes

GET /api/todo/{id} Obtener un elemento por Ninguna Tarea pendiente


identificador

POST /api/todo Agregar un nuevo elemento Tarea pendiente Tarea pendiente

PUT /api/todo/{id} Actualizar un elemento Tarea pendiente Ninguna


existente

DELETE /api/todo/{id} Eliminar un elemento Ninguna Ninguna

El siguiente diagrama muestra el diseño básico de la aplicación.

El cliente es todo lo que consume la API web (aplicación móvil, explorador, etcétera). En este tutorial no se
crea ningún cliente. Postman o curl se utilizan como el cliente para probar la aplicación.
Un modelo es un objeto que representa los datos de la aplicación. En este caso, el único modelo es una tarea
pendiente. Los modelos se representan como clases de C#, también conocidas como clases POCO (del
inglés Plain Old C# Object, objetos CRL antiguos sin formato).
Un controlador es un objeto que controla solicitudes HTTP y crea la respuesta HTTP. Esta aplicación tiene un
único controlador.
Para simplificar el tutorial, la aplicación no usa una base de datos persistente. La aplicación de ejemplo
almacena tareas pendientes en una base de datos en memoria.
Vea Introduction to ASP.NET Core MVC on macOS or Linux (Introducción a ASP.NET Core MVC en macOS o
Linux) para obtener un ejemplo en el que se usa una base de datos persistente.

Requisitos previos
Visual Studio for Mac

Crear el proyecto
En Visual Studio, seleccione Archivo > Nueva solución.

Seleccione Aplicación .NET Core > API web de ASP.NET Core > Siguiente.
Escriba TodoApi en Nombre del proyecto y haga clic en Crear.

Iniciar la aplicación
En Visual Studio, seleccione Ejecutar > Iniciar con depuración para iniciar la aplicación. Visual Studio inicia un
explorador y se desplaza a http://localhost:5000 . Obtendrá un error HTTP 404 (No encontrado). Cambie la
dirección URL a http://localhost:<port>/api/values . Se muestran los datos de ValuesController :
["value1","value2"]

Agregar compatibilidad con Entity Framework Core


Instale el proveedor de base de datos Entity Framework Core InMemory. Este proveedor de base de datos permite
usar Entity Framework Core con una base de datos en memoria.
En el menú Proyecto, seleccione Agregar paquetes NuGet.
Como alternativa, puede hacer clic con el botón derecho en Dependencias y seleccionar Agregar
paquetes.
Escriba EntityFrameworkCore.InMemory en el cuadro de búsqueda.
Seleccione Microsoft.EntityFrameworkCore.InMemory y, luego, Agregar paquete.
Agregar una clase de modelo
Un modelo es un objeto que representa los datos de la aplicación. En este caso, el único modelo es una tarea
pendiente.
En el Explorador de soluciones, haga clic con el botón derecho en el proyecto. Seleccione Agregar > Nueva
carpeta. Asigne a la carpeta el nombre Modelos.
NOTE
Puede colocar clases de modelo en cualquier lugar del proyecto, pero la carpeta Models se usa por convención.

Haga clic con el botón derecho en la carpeta Modelos y seleccione Agregar > Nuevo archivo > General > Clase
vacía. Denomine la clase TodoItem y, después, haga clic en Nuevo.
Reemplace el código generado por el siguiente:

namespace TodoApi.Models
{
public class TodoItem
{
public long Id { get; set; }
public string Name { get; set; }
public bool IsComplete { get; set; }
}
}

La base de datos genera el Id cuando se crea TodoItem .


Crear el contexto de base de datos
El contexto de base de datos es la clase principal que coordina la funcionalidad de Entity Framework para un
modelo de datos determinado. Esta clase se crea derivándola de la clase Microsoft.EntityFrameworkCore.DbContext .
Agregue una clase TodoContext a la carpeta Modelos.

using Microsoft.EntityFrameworkCore;

namespace TodoApi.Models
{
public class TodoContext : DbContext
{
public TodoContext(DbContextOptions<TodoContext> options)
: base(options)
{
}

public DbSet<TodoItem> TodoItems { get; set; }


}
}

Registrar el contexto de base de datos


En este paso, el contexto de base de datos se registra con el contenedor de inserción de dependencias. Los servicios
(por ejemplo, el contexto de la base de datos) que se registran con el contenedor de inserción de dependencias (DI)
están disponibles para los controladores.
Registre el contexto de la base de datos con el contenedor de servicio mediante la compatibilidad integrada para
inserción de dependencias. Reemplace el contenido del archivo Startup.cs con el código siguiente:
[!code-csharp]
[!code-csharp]
El código anterior:
Quita el código no usado.
Especifica que se inserte una base de datos en memoria en el contenedor de servicios.

Agregar un controlador
En el Explorador de soluciones, en la carpeta Controladores, agregue la clase TodoController .
Reemplace el código generado con el siguiente:
[!code-csharp]
El código anterior define una clase de controlador de API sin métodos. En las secciones siguientes, se agregan
métodos para implementar la API.
[!code-csharp]
El código anterior define una clase de controlador de API sin métodos. En las secciones siguientes, se agregan
métodos para implementar la API. La clase se anota con un atributo [ApiController] para habilitar algunas
características muy prácticas. Para más información sobre las características que el atributo habilita, vea Anotación
de una clase con ApiControllerAttribute.
El constructor del controlador usa la inserción de dependencias para insertar el contexto de base de datos (
TodoContext ) en el controlador. El contexto de base de datos se usa en cada uno de los métodos CRUD del
controlador. El constructor agrega un elemento a la base de datos en memoria si no existe ninguno.

Tareas pendientes
Para obtener tareas pendientes, agregue estos métodos a la clase TodoController :
[!code-csharp]
[!code-csharp]
Estos métodos implementan los dos métodos GET:
GET /api/todo
GET /api/todo/{id}

Esta es una respuesta HTTP de ejemplo del método GetAll :

[
{
"id": 1,
"name": "Item1",
"isComplete": false
}
]

Más adelante en el tutorial, veremos cómo se puede ver la respuesta HTTP por medio de Postman o curl.
Enrutamiento y rutas URL
El atributo [HttpGet] indica un método que responde a una solicitud HTTP GET. La ruta de dirección URL para
cada método se construye como sigue:
Tome la cadena de plantilla en el atributo Route del controlador:
[!code-csharp]
[!code-csharp]
Reemplace [controller] por el nombre del controlador, que es el nombre de clase de controlador sin el sufijo
"Controller". En este ejemplo, el nombre de clase de controlador es TodoController y el nombre de raíz es
"todo". El enrutamiento en ASP.NET Core no distingue entre mayúsculas y minúsculas.
Si el atributo [HttpGet] tiene una plantilla de ruta (como [HttpGet("/products")] ), anexiónela a la ruta de
acceso. En este ejemplo no se usa una plantilla. Para más información, vea Enrutamiento mediante atributos con
atributos Http[Verb].
En el siguiente método GetById , "{id}" es una variable de marcador de posición correspondiente al identificador
único de la tarea pendiente. Cuando GetById se invoca, asigna el valor "{id}" de la dirección URL al parámetro
id del método.

[!code-csharp]
[!code-csharp]
Name = "GetTodo" crea una ruta con nombre. Rutas con nombre:
Permita que la aplicación cree un vínculo HTTP mediante el nombre de ruta.
Se explican más adelante en el tutorial.
Valores devueltos
El método GetAll devuelve una colección de objetos TodoItem . MVC serializa automáticamente el objeto a JSON
y escribe el JSON en el cuerpo del mensaje de respuesta. El código de respuesta para este método es 200,
suponiendo que no haya ninguna excepción no controlada. Las excepciones no controladas se convierten en
errores 5xx.
En cambio, el método GetById devuelve el tipo más general IActionResult, que representa una amplia gama de
tipos de valor devuelto. GetById tiene dos tipos de valor devueltos distintos:
Si no hay ningún elemento que coincida con el identificador solicitado, el método devuelve un error 404.
Devolver NotFound genera una respuesta HTTP 404.
En caso contrario, el método devuelve 200 con un cuerpo de respuesta JSON. Devolver Ok genera una
respuesta HTTP 200.
En cambio, el método GetById devuelve el tipo ActionResult<T>, que representa una amplia gama de tipos de
valor devuelto. GetById tiene dos tipos de valor devueltos distintos:
Si no hay ningún elemento que coincida con el identificador solicitado, el método devuelve un error 404.
Devolver NotFound genera una respuesta HTTP 404.
En caso contrario, el método devuelve 200 con un cuerpo de respuesta JSON. Devolver item genera una
respuesta HTTP 200.
Iniciar la aplicación
En Visual Studio, seleccione Ejecutar > Iniciar con depuración para iniciar la aplicación. Visual Studio inicia un
explorador y navega hasta http://localhost:<port> , donde <port> es un número de puerto elegido
aleatoriamente. Obtendrá un error HTTP 404 (No encontrado). Cambie la dirección URL a
http://localhost:<port>/api/values . Se muestran los datos de ValuesController :

["value1","value2"]

Vaya al controlador Todo en http://localhost:<port>/api/todo . Se devuelve el siguiente JSON:

[{"key":1,"name":"Item1","isComplete":false}]
Implementar las otras operaciones CRUD
Vamos a agregar los métodos Create , Update y Delete al controlador. Estos métodos son variaciones de un
tema, así que solo mostraré el código y comentaré las diferencias principales. Compile el proyecto después de
agregar o cambiar el código.
Crear
[!code-csharp]
El código anterior responde a un método HTTP POST, como se puede apreciar por el atributo [HttpPost]. El
atributo [FromBody] indica a MVC que obtenga el valor de la tarea pendiente del cuerpo de la solicitud HTTP.
[!code-csharp]
El código anterior responde a un método HTTP POST, como se puede apreciar por el atributo [HttpPost]. MVC
obtiene el valor de la tarea pendiente del cuerpo de la solicitud HTTP.
El método CreatedAtRoute devuelve una respuesta 201. Se trata de la respuesta estándar de un método HTTP
POST que crea un recurso en el servidor. CreatedAtRoute también agrega un encabezado de ubicación a la
respuesta. El encabezado de ubicación especifica el URI de la tarea pendiente recién creada. Vea 10.2.2 201 Created
(10.2.2 201 creada).
Usar Postman para enviar una solicitud de creación
Inicie la aplicación (Ejecutar > Iniciar con depuración).
Abra Postman.

Actualice el número de puerto en la dirección URL de localhost.


Establezca el método HTTP en POST.
Haga clic en la pestaña Body (Cuerpo).
Seleccione el botón de radio Raw (Sin formato).
Establezca el tipo en JSON (application/json).
Escriba un cuerpo de solicitud con una tarea pendiente parecida al siguiente JSON:

{
"name":"walk dog",
"isComplete":true
}

Haga clic en el botón Send (Enviar).

TIP
Si no aparece ninguna respuesta tras hacer clic en Send (Enviar), deshabilite la opción SSL certification verification
(Comprobación de certificación SSL). La encontrará en File > Settings (Archivo > Configuración). Vuelva a hacer clic en el
botón Send (Enviar) después de deshabilitar la configuración.

Haga clic en la pestaña Headers (Encabezados) del panel Response (Respuesta) y copie el valor de encabezado de
Location (Ubicación):

Puede usar el URI del encabezado Location (Ubicación) para tener acceso al recurso que ha creado. El método
Create devuelve CreatedAtRoute. El primer parámetro que se pasa a CreatedAtRoute representa la ruta con
nombre que se usa para generar la dirección URL. Recuerde que el método GetById creó la ruta con nombre
"GetTodo" :

[HttpGet("{id}", Name = "GetTodo")]

Actualizar
[!code-csharp]
[!code-csharp]
Update es similar a Create , pero usa HTTP PUT. La respuesta es 204 Sin contenido. Según la especificación HTTP,
una solicitud PUT requiere que el cliente envíe toda la entidad actualizada, no solo los deltas. Para admitir
actualizaciones parciales, use HTTP PATCH.

{
"key": 1,
"name": "walk dog",
"isComplete": true
}

Eliminar
[HttpDelete("{id}")]
public IActionResult Delete(long id)
{
var todo = _context.TodoItems.Find(id);
if (todo == null)
{
return NotFound();
}

_context.TodoItems.Remove(todo);
_context.SaveChanges();
return NoContent();
}

La respuesta es 204 Sin contenido.

Llamar a Web API con jQuery


En esta sección, se agrega una página HTML que usa jQuery para llamar a Web API. jQuery inicia la solicitud y
actualiza la página con los detalles de la respuesta de la API.
Configure el proyecto para atender archivos estáticos y para permitir la asignación de archivos predeterminada.
Esto se logra invocando los métodos de extensión UseStaticFiles y UseDefaultFiles en Startup.Configure. Para
obtener más información, consulte Archivos estáticos.
public void Configure(IApplicationBuilder app)
{
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseMvc();
}

Agregue un archivo HTML denominado index.html al directorio wwwroot del proyecto. Reemplace el contenido
por el siguiente marcado:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>To-do CRUD</title>
<style>
input[type='submit'], button, [aria-label] {
cursor: pointer;
}

#spoiler {
display: none;
}

table {
font-family: Arial, sans-serif;
border: 1px solid;
border-collapse: collapse;
}

th {
background-color: #0066CC;
color: white;
}

td {
border: 1px solid;
padding: 5px;
}
</style>
</head>
<body>
<h1>To-do CRUD</h1>
<h3>Add</h3>
<form action="javascript:void(0);" method="POST" onsubmit="addItem()">
<input type="text" id="add-name" placeholder="New to-do">
<input type="submit" value="Add">
</form>

<div id="spoiler">
<h3>Edit</h3>
<form class="my-form">
<input type="hidden" id="edit-id">
<input type="checkbox" id="edit-isComplete">
<input type="text" id="edit-name">
<input type="submit" value="Edit">
<a onclick="closeInput()" aria-label="Close">&#10006;</a>
</form>
</div>

<p id="counter"></p>

<table>
<tr>
<th>Is Complete</th>
<th>Name</th>
<th>Name</th>
<th></th>
<th></th>
</tr>
<tbody id="todos"></tbody>
</table>

<script src="https://code.jquery.com/jquery-3.3.1.min.js"
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
crossorigin="anonymous"></script>
<script src="site.js"></script>
</body>
</html>

Agregue un archivo JavaScript denominado site.js al directorio wwwroot del proyecto. Reemplace el contenido por
el siguiente código:

const uri = 'api/todo';


let todos = null;
function getCount(data) {
const el = $('#counter');
let name = 'to-do';
if (data) {
if (data > 1) {
name = 'to-dos';
}
el.text(data + ' ' + name);
} else {
el.html('No ' + name);
}
}

$(document).ready(function () {
getData();
});

function getData() {
$.ajax({
type: 'GET',
url: uri,
success: function (data) {
$('#todos').empty();
getCount(data.length);
$.each(data, function (key, item) {
const checked = item.isComplete ? 'checked' : '';

$('<tr><td><input disabled="true" type="checkbox" ' + checked + '></td>' +


'<td>' + item.name + '</td>' +
'<td><button onclick="editItem(' + item.id + ')">Edit</button></td>' +
'<td><button onclick="deleteItem(' + item.id + ')">Delete</button></td>' +
'</tr>').appendTo($('#todos'));
});

todos = data;
}
});
}

function addItem() {
const item = {
'name': $('#add-name').val(),
'isComplete': false
};

$.ajax({
type: 'POST',
accepts: 'application/json',
url: uri,
url: uri,
contentType: 'application/json',
data: JSON.stringify(item),
error: function (jqXHR, textStatus, errorThrown) {
alert('here');
},
success: function (result) {
getData();
$('#add-name').val('');
}
});
}

function deleteItem(id) {
$.ajax({
url: uri + '/' + id,
type: 'DELETE',
success: function (result) {
getData();
}
});
}

function editItem(id) {
$.each(todos, function (key, item) {
if (item.id === id) {
$('#edit-name').val(item.name);
$('#edit-id').val(item.id);
$('#edit-isComplete').val(item.isComplete);
}
});
$('#spoiler').css({ 'display': 'block' });
}

$('.my-form').on('submit', function () {
const item = {
'name': $('#edit-name').val(),
'isComplete': $('#edit-isComplete').is(':checked'),
'id': $('#edit-id').val()
};

$.ajax({
url: uri + '/' + $('#edit-id').val(),
type: 'PUT',
accepts: 'application/json',
contentType: 'application/json',
data: JSON.stringify(item),
success: function (result) {
getData();
}
});

closeInput();
return false;
});

function closeInput() {
$('#spoiler').css({ 'display': 'none' });
}

Puede que sea necesario realizar un cambio en la configuración de inicio del proyecto de ASP.NET Core para
comprobar la página HTML localmente. Abra launchSettings.json en el directorio Properties del proyecto. Quite la
propiedad launchUrl para forzar a la aplicación a abrirse en index.html, esto es, el archivo predeterminado del
proyecto.
Existen varias formas de obtener jQuery. En el fragmento de código anterior, la biblioteca se carga desde una red
CDN. Este ejemplo es un ejemplo de CRUD completo de llamada a la API de jQuery. Existen más características en
este ejemplo para hacer que la experiencia sea más completa. Aquí verá algunas explicaciones sobre las llamadas a
la API.
Obtener una lista de tareas pendientes
Para obtener una lista de tareas pendientes, envíe una solicitud HTTP GET a /api/todo.
La función de JQuery ajax envía una solicitud AJAX a la API, que devuelve código JSON que representa un objeto
o una matriz. Esta función puede controlar todas las formas de interacción de HTTP enviando una solicitud HTTP a
la url especificada. GET se emite como type . La función de devolución de llamada success se invoca si la
solicitud se realiza correctamente. En la devolución de llamada, el DOM se actualiza con la información de la tarea
pendiente.

$(document).ready(function () {
getData();
});

function getData() {
$.ajax({
type: 'GET',
url: uri,
success: function (data) {
$('#todos').empty();
getCount(data.length);
$.each(data, function (key, item) {
const checked = item.isComplete ? 'checked' : '';

$('<tr><td><input disabled="true" type="checkbox" ' + checked + '></td>' +


'<td>' + item.name + '</td>' +
'<td><button onclick="editItem(' + item.id + ')">Edit</button></td>' +
'<td><button onclick="deleteItem(' + item.id + ')">Delete</button></td>' +
'</tr>').appendTo($('#todos'));
});

todos = data;
}
});
}

Agregar una tarea pendiente


Para agregar una tarea pendiente, envíe una solicitud HTTP POST a /api/todo. El cuerpo de la solicitud debe
contener un objeto de tarea pendiente. La función ajax usa POST para llamar a la API. En las solicitudes POST y
PUT , el cuerpo de la solicitud representa los datos enviados a la API. La API espera un cuerpo de solicitud con
formato JSON. Las opciones accepts y contentType se establecen en application/json para clasificar el tipo de
medio que se va a recibir y a enviar respectivamente. Los datos se convierten en un objeto JSON usando
JSON.stringify . Cuando la API devuelve un código de estado correcto, se invoca la función getData para
actualizar la tabla HTML.
function addItem() {
const item = {
'name': $('#add-name').val(),
'isComplete': false
};

$.ajax({
type: 'POST',
accepts: 'application/json',
url: uri,
contentType: 'application/json',
data: JSON.stringify(item),
error: function (jqXHR, textStatus, errorThrown) {
alert('here');
},
success: function (result) {
getData();
$('#add-name').val('');
}
});
}

Actualizar una tarea pendiente


Actualizar una tarea pendiente es muy parecido a agregarla, ya que ambos procesos se basan en el cuerpo de
solicitud. En este caso, la única diferencia real entre ambos es que url cambia para reflejar el identificador único
del elemento, y type es PUT .

$.ajax({
url: uri + '/' + $('#edit-id').val(),
type: 'PUT',
accepts: 'application/json',
contentType: 'application/json',
data: JSON.stringify(item),
success: function (result) {
getData();
}
});

Eliminar una tarea pendiente


Para eliminar una tarea pendiente, hay que establecer type de la llamada de AJAX en DELETE y especificar el
identificador único de la tarea en la dirección URL.

$.ajax({
url: uri + '/' + id,
type: 'DELETE',
success: function (result) {
getData();
}
});

Pasos siguientes
Para más información sobre cómo usar una base de datos persistente, vea:
Creación de una aplicación web de páginas de Razor con ASP.NET Core
Trabajo con datos en ASP.NET Core
Páginas de ayuda de ASP.NET Core Web API mediante Swagger
Enrutamiento a acciones del controlador
Compilación de API web con ASP.NET Core
Tipos de valor devuelto de acción del controlador
Para obtener información sobre la implementación de una API, como en Azure App Service, vea la
documentación sobre Hospedaje e implementación.
Vea o descargue el código de ejemplo. Vea cómo descargarlo.
Crear una API web con ASP.NET Core y Visual
Studio para Windows
21/06/2018 • 25 minutes to read • Edit Online

Por Rick Anderson y Mike Wasson


En este tutorial se compila una API web para administrar una lista de tareas pendientes. No se crea ninguna
interfaz de usuario (UI).
Hay tres versiones de este tutorial:
Windows: API web con Visual Studio para Windows (este tutorial)
macOS: API web con Visual Studio para Mac
macOS, Linux y Windows: API web con Visual Studio Code

Información general
En este tutorial se crea la siguiente API:

API DESCRIPTION CUERPO DE LA SOLICITUD CUERPO DE LA RESPUESTA

GET /api/todo Obtener todas las tareas Ninguna Matriz de tareas pendientes
pendientes

GET /api/todo/{id} Obtener un elemento por Ninguna Tarea pendiente


identificador

POST /api/todo Agregar un nuevo elemento Tarea pendiente Tarea pendiente

PUT /api/todo/{id} Actualizar un elemento Tarea pendiente Ninguna


existente

DELETE /api/todo/{id} Eliminar un elemento Ninguna Ninguna

El siguiente diagrama muestra el diseño básico de la aplicación.

El cliente es todo lo que consume la API web (aplicación móvil, explorador, etcétera). En este tutorial no se
crea ningún cliente. Postman o curl se utilizan como el cliente para probar la aplicación.
Un modelo es un objeto que representa los datos de la aplicación. En este caso, el único modelo es una
tarea pendiente. Los modelos se representan como clases de C#, también conocidas como clases POCO
(del inglés Plain Old C# Object, objetos CRL antiguos sin formato).
Un controlador es un objeto que controla solicitudes HTTP y crea la respuesta HTTP. Esta aplicación tiene
un único controlador.
Para simplificar el tutorial, la aplicación no usa una base de datos persistente. La aplicación de ejemplo
almacena tareas pendientes en una base de datos en memoria.

Requisitos previos
Visual Studio for Windows.
Select the ASP.NET and web development workload.
.Net Core 2.1 SDK

Crear el proyecto
Haga lo siguiente para descargar Visual Studio:
En el menú Archivo, seleccione Nuevo > Proyecto.
Seleccione la plantilla Aplicación web ASP.NET Core. Denomine el proyecto TodoApi y haga clic en
Aceptar.
En el cuadro de diálogo Nueva aplicación web ASP.NET Core - TodoApi, seleccione la versión ASP.NET
Core. Seleccione la plantilla API y haga clic en Aceptar. No seleccione Habilitar compatibilidad con
Docker.
Iniciar la aplicación
En Visual Studio, presione CTRL+F5 para iniciar la aplicación. Visual Studio inicia un explorador y navega hasta
http://localhost:<port>/api/values , donde <port> es un número de puerto elegido aleatoriamente. En
Chrome, Microsoft Edge y Firefox se muestra la salida siguiente:

["value1","value2"]

Si usa Internet Explorer, se le pedirá que guarde un archivo values.json.


Agregar una clase de modelo
Un modelo es un objeto que representa los datos de la aplicación. En este caso, el único modelo es una tarea
pendiente.
En el Explorador de soluciones, haga clic con el botón derecho en el proyecto. Seleccione Agregar > Nueva
carpeta. Asigne a la carpeta el nombre Models.

NOTE
Las clases del modelo pueden ir en cualquier parte del proyecto. La carpeta Models se usa por convención para las clases
de modelos.

En el Explorador de soluciones, haga clic con el botón derecho en la carpeta de modelos y seleccione Agregar >
Clase. Denomine la clase TodoItem y, después, haga clic en Agregar.
Actualice la clase TodoItem por el siguiente código:
namespace TodoApi.Models
{
public class TodoItem
{
public long Id { get; set; }
public string Name { get; set; }
public bool IsComplete { get; set; }
}
}

La base de datos genera el Id cuando se crea TodoItem .


Crear el contexto de base de datos
El contexto de base de datos es la clase principal que coordina la funcionalidad de Entity Framework para un
modelo de datos determinado. Esta clase se crea derivándola de la clase
Microsoft.EntityFrameworkCore.DbContext .

En el Explorador de soluciones, haga clic con el botón derecho en la carpeta de modelos y seleccione Agregar >
Clase. Denomine la clase TodoContext y, después, haga clic en Agregar.
Reemplace la clase por el siguiente código:

using Microsoft.EntityFrameworkCore;

namespace TodoApi.Models
{
public class TodoContext : DbContext
{
public TodoContext(DbContextOptions<TodoContext> options)
: base(options)
{
}

public DbSet<TodoItem> TodoItems { get; set; }


}
}

Registrar el contexto de base de datos


En este paso, el contexto de base de datos se registra con el contenedor de inserción de dependencias. Los
servicios (por ejemplo, el contexto de la base de datos) que se registran con el contenedor de inserción de
dependencias (DI) están disponibles para los controladores.
Registre el contexto de la base de datos con el contenedor de servicio mediante la compatibilidad integrada para
inserción de dependencias. Reemplace el contenido del archivo Startup.cs con el código siguiente:
[!code-csharp]
[!code-csharp]
El código anterior:
Quita el código no usado.
Especifica que se inserte una base de datos en memoria en el contenedor de servicios.
Adición de un controlador
En el Explorador de soluciones, haga clic con el botón derecho en la carpeta Controladores. Seleccione Agregar
> Nuevo elemento. En el cuadro de diálogo Agregar nuevo elemento, seleccione la plantilla Clase de
controlador de API. Denomine la clase TodoController y, después, haga clic en Agregar.
Reemplace la clase por el siguiente código:
[!code-csharp]
El código anterior define una clase de controlador de API sin métodos. En las secciones siguientes, se agregan
métodos para implementar la API.
[!code-csharp]
El código anterior define una clase de controlador de API sin métodos. En las secciones siguientes, se agregan
métodos para implementar la API. La clase se anota con un atributo [ApiController] para habilitar algunas
características muy prácticas. Para más información sobre las características que el atributo habilita, vea
Anotación de una clase con ApiControllerAttribute.
El constructor del controlador usa la inserción de dependencias para insertar el contexto de base de datos (
TodoContext ) en el controlador. El contexto de base de datos se usa en cada uno de los métodos CRUD del
controlador. El constructor agrega un elemento a la base de datos en memoria si no existe ninguno.

Tareas pendientes
Para obtener tareas pendientes, agregue estos métodos a la clase TodoController :
[!code-csharp]
[!code-csharp]
Estos métodos implementan los dos métodos GET:
GET /api/todo
GET /api/todo/{id}

Esta es una respuesta HTTP de ejemplo del método GetAll :


[
{
"id": 1,
"name": "Item1",
"isComplete": false
}
]

Más adelante en el tutorial, veremos cómo se puede ver la respuesta HTTP por medio de Postman o curl.
Enrutamiento y rutas URL
El atributo [HttpGet] indica un método que responde a una solicitud HTTP GET. La ruta de dirección URL para
cada método se construye como sigue:
Tome la cadena de plantilla en el atributo Route del controlador:
[!code-csharp]
[!code-csharp]
Reemplace [controller] por el nombre del controlador, que es el nombre de clase de controlador sin el
sufijo "Controller". En este ejemplo, el nombre de clase de controlador es TodoController y el nombre de raíz
es "todo". El enrutamiento en ASP.NET Core no distingue entre mayúsculas y minúsculas.
Si el atributo [HttpGet] tiene una plantilla de ruta (como [HttpGet("/products")] ), anexiónela a la ruta de
acceso. En este ejemplo no se usa una plantilla. Para más información, vea Enrutamiento mediante atributos
con atributos Http[Verb].
En el siguiente método GetById , "{id}" es una variable de marcador de posición correspondiente al
identificador único de la tarea pendiente. Cuando GetById se invoca, asigna el valor "{id}" de la dirección
URL al parámetro id del método.
[!code-csharp]
[!code-csharp]
Name = "GetTodo" crea una ruta con nombre. Rutas con nombre:
Permita que la aplicación cree un vínculo HTTP mediante el nombre de ruta.
Se explican más adelante en el tutorial.
Valores devueltos
El método GetAll devuelve una colección de objetos TodoItem . MVC serializa automáticamente el objeto a
JSON y escribe el JSON en el cuerpo del mensaje de respuesta. El código de respuesta para este método es
200, suponiendo que no haya ninguna excepción no controlada. Las excepciones no controladas se convierten
en errores 5xx.
En cambio, el método GetById devuelve el tipo más general IActionResult, que representa una amplia gama de
tipos de valor devuelto. GetById tiene dos tipos de valor devueltos distintos:
Si no hay ningún elemento que coincida con el identificador solicitado, el método devuelve un error 404.
Devolver NotFound genera una respuesta HTTP 404.
En caso contrario, el método devuelve 200 con un cuerpo de respuesta JSON. Devolver Ok genera una
respuesta HTTP 200.
En cambio, el método GetById devuelve el tipo ActionResult<T>, que representa una amplia gama de tipos de
valor devuelto. GetById tiene dos tipos de valor devueltos distintos:
Si no hay ningún elemento que coincida con el identificador solicitado, el método devuelve un error 404.
Devolver NotFound genera una respuesta HTTP 404.
En caso contrario, el método devuelve 200 con un cuerpo de respuesta JSON. Devolver item genera una
respuesta HTTP 200.
Iniciar la aplicación
En Visual Studio, presione CTRL+F5 para iniciar la aplicación. Visual Studio inicia un explorador y navega hasta
http://localhost:<port>/api/values , donde <port> es un número de puerto elegido aleatoriamente. Vaya al
controlador Todo en http://localhost:<port>/api/todo .

Implementar las otras operaciones CRUD


En las secciones siguientes, se agregan los métodos Create , Update y Delete al controlador.
Crear
Agregue el siguiente método Create :
[!code-csharp]
El código anterior es un método HTTP POST, según indica el atributo [HttpPost]. El atributo [FromBody] indica
a MVC que obtenga el valor de la tarea pendiente del cuerpo de la solicitud HTTP.
[!code-csharp]
El código anterior es un método HTTP POST, según indica el atributo [HttpPost]. MVC obtiene el valor de la
tarea pendiente del cuerpo de la solicitud HTTP.
El método CreatedAtRoute realiza las acciones siguientes:
Devuelve una respuesta 201. HTTP 201 es la respuesta estándar para un método HTTP POST que crea un
recurso en el servidor.
Agrega un encabezado de ubicación a la respuesta. El encabezado de ubicación especifica el URI de la tarea
pendiente recién creada. Vea 10.2.2 201 Created (10.2.2 201 creada).
Usa la ruta denominada "GetTodo" para crear la dirección URL. La ruta con nombre "GetTodo" se define en
GetById :

[!code-csharp]
[!code-csharp]
Usar Postman para enviar una solicitud de creación
Inicia la aplicación.
Abra Postman.
Actualice el número de puerto en la dirección URL de localhost.
Establezca el método HTTP en POST.
Haga clic en la pestaña Body (Cuerpo).
Seleccione el botón de radio Raw (Sin formato).
Establezca el tipo en JSON (application/json).
Escriba un cuerpo de solicitud con una tarea pendiente parecida al siguiente JSON:

{
"name":"walk dog",
"isComplete":true
}

Haga clic en el botón Send (Enviar).

TIP
Si no aparece ninguna respuesta tras hacer clic en Send (Enviar), deshabilite la opción SSL certification verification
(Comprobación de certificación SSL). La encontrará en File > Settings (Archivo > Configuración). Vuelva a hacer clic en el
botón Send (Enviar) después de deshabilitar la configuración.

Haga clic en la pestaña Headers (Encabezados) del panel Response (Respuesta) y copie el valor de encabezado
de Location (Ubicación):
El URI del encabezado de ubicación puede utilizarse para acceder al nuevo elemento.
Actualizar
Agregue el siguiente método Update :
[!code-csharp]
[!code-csharp]
Update es similar a Create , salvo por el hecho de que usa HTTP PUT. La respuesta es 204 Sin contenido.
Según la especificación HTTP, una solicitud PUT requiere que el cliente envíe toda la entidad actualizada, no solo
los deltas. Para admitir actualizaciones parciales, use HTTP PATCH.
Use Postman para actualizar el nombre de la tarea pendiente a "walk cat":
Eliminar
Agregue el siguiente método Delete :

[HttpDelete("{id}")]
public IActionResult Delete(long id)
{
var todo = _context.TodoItems.Find(id);
if (todo == null)
{
return NotFound();
}

_context.TodoItems.Remove(todo);
_context.SaveChanges();
return NoContent();
}

La respuesta de Delete es 204 Sin contenido.


Use Postman para eliminar la tarea pendiente:
Llamar a Web API con jQuery
En esta sección, se agrega una página HTML que usa jQuery para llamar a Web API. jQuery inicia la solicitud y
actualiza la página con los detalles de la respuesta de la API.
Configure el proyecto para atender archivos estáticos y para permitir la asignación de archivos predeterminada.
Esto se logra invocando los métodos de extensión UseStaticFiles y UseDefaultFiles en Startup.Configure. Para
obtener más información, consulte Archivos estáticos.

public void Configure(IApplicationBuilder app)


{
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseMvc();
}

Agregue un archivo HTML denominado index.html al directorio wwwroot del proyecto. Reemplace el contenido
por el siguiente marcado:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>To-do CRUD</title>
<style>
input[type='submit'], button, [aria-label] {
cursor: pointer;
}
#spoiler {
display: none;
}

table {
font-family: Arial, sans-serif;
border: 1px solid;
border-collapse: collapse;
}

th {
background-color: #0066CC;
color: white;
}

td {
border: 1px solid;
padding: 5px;
}
</style>
</head>
<body>
<h1>To-do CRUD</h1>
<h3>Add</h3>
<form action="javascript:void(0);" method="POST" onsubmit="addItem()">
<input type="text" id="add-name" placeholder="New to-do">
<input type="submit" value="Add">
</form>

<div id="spoiler">
<h3>Edit</h3>
<form class="my-form">
<input type="hidden" id="edit-id">
<input type="checkbox" id="edit-isComplete">
<input type="text" id="edit-name">
<input type="submit" value="Edit">
<a onclick="closeInput()" aria-label="Close">&#10006;</a>
</form>
</div>

<p id="counter"></p>

<table>
<tr>
<th>Is Complete</th>
<th>Name</th>
<th></th>
<th></th>
</tr>
<tbody id="todos"></tbody>
</table>

<script src="https://code.jquery.com/jquery-3.3.1.min.js"
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
crossorigin="anonymous"></script>
<script src="site.js"></script>
</body>
</html>

Agregue un archivo JavaScript denominado site.js al directorio wwwroot del proyecto. Reemplace el contenido
por el siguiente código:

const uri = 'api/todo';


let todos = null;
function getCount(data) {
const el = $('#counter');
let name = 'to-do';
let name = 'to-do';
if (data) {
if (data > 1) {
name = 'to-dos';
}
el.text(data + ' ' + name);
} else {
el.html('No ' + name);
}
}

$(document).ready(function () {
getData();
});

function getData() {
$.ajax({
type: 'GET',
url: uri,
success: function (data) {
$('#todos').empty();
getCount(data.length);
$.each(data, function (key, item) {
const checked = item.isComplete ? 'checked' : '';

$('<tr><td><input disabled="true" type="checkbox" ' + checked + '></td>' +


'<td>' + item.name + '</td>' +
'<td><button onclick="editItem(' + item.id + ')">Edit</button></td>' +
'<td><button onclick="deleteItem(' + item.id + ')">Delete</button></td>' +
'</tr>').appendTo($('#todos'));
});

todos = data;
}
});
}

function addItem() {
const item = {
'name': $('#add-name').val(),
'isComplete': false
};

$.ajax({
type: 'POST',
accepts: 'application/json',
url: uri,
contentType: 'application/json',
data: JSON.stringify(item),
error: function (jqXHR, textStatus, errorThrown) {
alert('here');
},
success: function (result) {
getData();
$('#add-name').val('');
}
});
}

function deleteItem(id) {
$.ajax({
url: uri + '/' + id,
type: 'DELETE',
success: function (result) {
getData();
}
});
}

function editItem(id) {
function editItem(id) {
$.each(todos, function (key, item) {
if (item.id === id) {
$('#edit-name').val(item.name);
$('#edit-id').val(item.id);
$('#edit-isComplete').val(item.isComplete);
}
});
$('#spoiler').css({ 'display': 'block' });
}

$('.my-form').on('submit', function () {
const item = {
'name': $('#edit-name').val(),
'isComplete': $('#edit-isComplete').is(':checked'),
'id': $('#edit-id').val()
};

$.ajax({
url: uri + '/' + $('#edit-id').val(),
type: 'PUT',
accepts: 'application/json',
contentType: 'application/json',
data: JSON.stringify(item),
success: function (result) {
getData();
}
});

closeInput();
return false;
});

function closeInput() {
$('#spoiler').css({ 'display': 'none' });
}

Puede que sea necesario realizar un cambio en la configuración de inicio del proyecto de ASP.NET Core para
comprobar la página HTML localmente. Abra launchSettings.json en el directorio Properties del proyecto. Quite
la propiedad launchUrl para forzar a la aplicación a abrirse en index.html, esto es, el archivo predeterminado
del proyecto.
Existen varias formas de obtener jQuery. En el fragmento de código anterior, la biblioteca se carga desde una
red CDN. Este ejemplo es un ejemplo de CRUD completo de llamada a la API de jQuery. Existen más
características en este ejemplo para hacer que la experiencia sea más completa. Aquí verá algunas explicaciones
sobre las llamadas a la API.
Obtener una lista de tareas pendientes
Para obtener una lista de tareas pendientes, envíe una solicitud HTTP GET a /api/todo.
La función de JQuery ajax envía una solicitud AJAX a la API, que devuelve código JSON que representa un
objeto o una matriz. Esta función puede controlar todas las formas de interacción de HTTP enviando una
solicitud HTTP a la url especificada. GET se emite como type . La función de devolución de llamada success
se invoca si la solicitud se realiza correctamente. En la devolución de llamada, el DOM se actualiza con la
información de la tarea pendiente.
$(document).ready(function () {
getData();
});

function getData() {
$.ajax({
type: 'GET',
url: uri,
success: function (data) {
$('#todos').empty();
getCount(data.length);
$.each(data, function (key, item) {
const checked = item.isComplete ? 'checked' : '';

$('<tr><td><input disabled="true" type="checkbox" ' + checked + '></td>' +


'<td>' + item.name + '</td>' +
'<td><button onclick="editItem(' + item.id + ')">Edit</button></td>' +
'<td><button onclick="deleteItem(' + item.id + ')">Delete</button></td>' +
'</tr>').appendTo($('#todos'));
});

todos = data;
}
});
}

Agregar una tarea pendiente


Para agregar una tarea pendiente, envíe una solicitud HTTP POST a /api/todo. El cuerpo de la solicitud debe
contener un objeto de tarea pendiente. La función ajax usa POST para llamar a la API. En las solicitudes POST y
PUT , el cuerpo de la solicitud representa los datos enviados a la API. La API espera un cuerpo de solicitud con
formato JSON. Las opciones accepts y contentType se establecen en application/json para clasificar el tipo
de medio que se va a recibir y a enviar respectivamente. Los datos se convierten en un objeto JSON usando
JSON.stringify . Cuando la API devuelve un código de estado correcto, se invoca la función getData para
actualizar la tabla HTML.

function addItem() {
const item = {
'name': $('#add-name').val(),
'isComplete': false
};

$.ajax({
type: 'POST',
accepts: 'application/json',
url: uri,
contentType: 'application/json',
data: JSON.stringify(item),
error: function (jqXHR, textStatus, errorThrown) {
alert('here');
},
success: function (result) {
getData();
$('#add-name').val('');
}
});
}

Actualizar una tarea pendiente


Actualizar una tarea pendiente es muy parecido a agregarla, ya que ambos procesos se basan en el cuerpo de
solicitud. En este caso, la única diferencia real entre ambos es que url cambia para reflejar el identificador
único del elemento, y type es PUT .

$.ajax({
url: uri + '/' + $('#edit-id').val(),
type: 'PUT',
accepts: 'application/json',
contentType: 'application/json',
data: JSON.stringify(item),
success: function (result) {
getData();
}
});

Eliminar una tarea pendiente


Para eliminar una tarea pendiente, hay que establecer type de la llamada de AJAX en DELETE y especificar el
identificador único de la tarea en la dirección URL.

$.ajax({
url: uri + '/' + id,
type: 'DELETE',
success: function (result) {
getData();
}
});

Pasos siguientes
Para más información sobre cómo usar una base de datos persistente, vea:
Creación de una aplicación web de páginas de Razor con ASP.NET Core
Trabajo con datos en ASP.NET Core
Páginas de ayuda de ASP.NET Core Web API mediante Swagger
Enrutamiento a acciones del controlador
Compilación de API web con ASP.NET Core
Tipos de valor devuelto de acción del controlador
Para obtener información sobre la implementación de una API, como en Azure App Service, vea la
documentación sobre Hospedaje e implementación.
Vea o descargue el código de ejemplo. Vea cómo descargarlo.
Crear servicios back-end para aplicaciones móviles
nativas con ASP.NET Core
25/06/2018 • 14 minutes to read • Edit Online

Por Steve Smith


Las aplicaciones móviles pueden comunicarse fácilmente con servicios back-end de ASP.NET Core.
Ver o descargar código de ejemplo de servicios back-end

La aplicación móvil nativa de ejemplo


Este tutorial muestra cómo crear servicios back-end mediante ASP.NET Core MVC para admitir aplicaciones
móviles nativas. Usa la aplicación Xamarin Forms ToDoRest como su cliente nativo, lo que incluye clientes nativos
independientes para dispositivos Android, iOS, Windows Universal y Windows Phone. Puede seguir el tutorial
vinculado para crear la aplicación nativa (e instalar las herramientas de Xamarin gratuitas necesarias), así como
descargar la solución de ejemplo de Xamarin. El ejemplo de Xamarin incluye un proyecto de servicios de ASP.NET
Web API 2, que sustituye a las aplicaciones de ASP.NET Core de este artículo (sin cambios requeridos por el
cliente).
Características
La aplicación ToDoRest permite enumerar, agregar, eliminar y actualizar tareas pendientes. Cada tarea tiene un
identificador, un nombre, notas y una propiedad que indica si ya se ha realizado.
La vista principal de las tareas, como se muestra anteriormente, indica el nombre de cada tarea e indica si se ha
realizado con una marca de verificación.
Al pulsar el icono + se abre un cuadro de diálogo para agregar un elemento:

Al pulsar un elemento en la pantalla de la lista principal se abre un cuadro de diálogo de edición, donde se puede
modificar el nombre del elemento, las notas y la configuración de Done (Listo), o se puede eliminar el elemento:
Este ejemplo está configurado para usar de forma predeterminada servicios back-end hospedados en
developer.xamarin.com, lo que permite operaciones de solo lectura. Para probarlo usted mismo con la aplicación de
ASP.NET Core que creó en la siguiente sección que se ejecuta en el equipo, debe actualizar la constante RestUrl de
la aplicación. Vaya hasta el proyecto ToDoREST y abra el archivo Constants.cs. Reemplace RestUrl con una
dirección URL que incluya la dirección IP de su equipo (no localhost ni 127.0.0.1, puesto que esta dirección se usa
desde el emulador de dispositivo, no desde el equipo). Incluya también el número de puerto (5000). Para
comprobar que los servicios funcionan con un dispositivo, asegúrese de que no tiene un firewall activo que bloquea
el acceso a este puerto.

// URL of REST service (Xamarin ReadOnly Service)


//public static string RestUrl = "http://developer.xamarin.com:8081/api/todoitems{0}";

// use your machine's IP address


public static string RestUrl = "http://192.168.1.207:5000/api/todoitems/{0}";

Creación del proyecto de ASP.NET Core


Cree una aplicación web de ASP.NET Core en Visual Studio. Elija la plantilla de API web y Sin autenticación.
Denomine el proyecto ToDoApi.
La aplicación debería responder a todas las solicitudes realizadas al puerto 5000. Actualice Program.cs para que
incluya .UseUrls("http://*:5000") para conseguir lo siguiente:

var host = new WebHostBuilder()


.UseKestrel()
.UseUrls("http://*:5000")
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<Startup>()
.Build();

NOTE
Asegúrese de que ejecuta la aplicación directamente, en lugar de tras IIS Express, que omite las solicitudes no locales de forma
predeterminada. Ejecute dotnet run desde un símbolo del sistema o elija el perfil del nombre de aplicación en la lista
desplegable de destino de depuración en la barra de herramientas de Visual Studio.

Agregue una clase de modelo para representar las tareas pendientes. Marque los campos obligatorios mediante el
atributo [Required] :
using System.ComponentModel.DataAnnotations;

namespace ToDoApi.Models
{
public class ToDoItem
{
[Required]
public string ID { get; set; }

[Required]
public string Name { get; set; }

[Required]
public string Notes { get; set; }

public bool Done { get; set; }


}
}

Los métodos de API necesitan alguna manera de trabajar con los datos. Use la misma interfaz de IToDoRepository
que usa el ejemplo original de Xamarin:

using System.Collections.Generic;
using ToDoApi.Models;

namespace ToDoApi.Interfaces
{
public interface IToDoRepository
{
bool DoesItemExist(string id);
IEnumerable<ToDoItem> All { get; }
ToDoItem Find(string id);
void Insert(ToDoItem item);
void Update(ToDoItem item);
void Delete(string id);
}
}

En este ejemplo, la implementación usa solo una colección de elementos privada:

using System.Collections.Generic;
using System.Linq;
using ToDoApi.Interfaces;
using ToDoApi.Models;

namespace ToDoApi.Services
{
public class ToDoRepository : IToDoRepository
{
private List<ToDoItem> _toDoList;

public ToDoRepository()
{
InitializeData();
}

public IEnumerable<ToDoItem> All


{
get { return _toDoList; }
}

public bool DoesItemExist(string id)


{
return _toDoList.Any(item => item.ID == id);
return _toDoList.Any(item => item.ID == id);
}

public ToDoItem Find(string id)


{
return _toDoList.FirstOrDefault(item => item.ID == id);
}

public void Insert(ToDoItem item)


{
_toDoList.Add(item);
}

public void Update(ToDoItem item)


{
var todoItem = this.Find(item.ID);
var index = _toDoList.IndexOf(todoItem);
_toDoList.RemoveAt(index);
_toDoList.Insert(index, item);
}

public void Delete(string id)


{
_toDoList.Remove(this.Find(id));
}

private void InitializeData()


{
_toDoList = new List<ToDoItem>();

var todoItem1 = new ToDoItem


{
ID = "6bb8a868-dba1-4f1a-93b7-24ebce87e243",
Name = "Learn app development",
Notes = "Attend Xamarin University",
Done = true
};

var todoItem2 = new ToDoItem


{
ID = "b94afb54-a1cb-4313-8af3-b7511551b33b",
Name = "Develop apps",
Notes = "Use Xamarin Studio/Visual Studio",
Done = false
};

var todoItem3 = new ToDoItem


{
ID = "ecfa6f80-3671-4911-aabe-63cc442c1ecf",
Name = "Publish apps",
Notes = "All app stores",
Done = false,
};

_toDoList.Add(todoItem1);
_toDoList.Add(todoItem2);
_toDoList.Add(todoItem3);
}
}
}

Configure la implementación en Startup.cs:


public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddMvc();

services.AddSingleton<IToDoRepository,ToDoRepository>();
}

En este punto, está listo para crear el ToDoItemsController.

TIP
Obtenga más información sobre cómo crear API web en Cree su primera API web con ASP.NET Core MVC y Visual Studio.

Crear el controlador
Agregue un nuevo controlador para el proyecto: ToDoItemsController. Debe heredar de
Microsoft.AspNetCore.Mvc.Controller. Agregue un atributo Route para indicar que el controlador controlará las
solicitudes realizadas a las rutas de acceso que comiencen con api/todoitems . El token [controller] de la ruta se
sustituye por el nombre del controlador (si se omite el sufijo Controller ) y es especialmente útil para las rutas
globales. Obtenga más información sobre el enrutamiento.
El controlador necesita un IToDoRepository para funcionar. Solicite una instancia de este tipo a través del
constructor del controlador. En tiempo de ejecución, esta instancia se proporcionará con la compatibilidad del
marco con la inserción de dependencias.

using System;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using ToDoApi.Interfaces;
using ToDoApi.Models;

namespace ToDoApi.Controllers
{
[Route("api/[controller]")]
public class ToDoItemsController : Controller
{
private readonly IToDoRepository _toDoRepository;

public ToDoItemsController(IToDoRepository toDoRepository)


{
_toDoRepository = toDoRepository;
}

Esta API es compatible con cuatro verbos HTTP diferentes para realizar operaciones CRUD (creación, lectura,
actualización, eliminación) en el origen de datos. La más simple de ellas es la operación de lectura, que corresponde
a una solicitud HTTP GET.
Leer elementos
La solicitud de una lista de elementos se realiza con una solicitud GET al método List . El atributo [HttpGet] en el
método List indica que esta acción solo debe controlar las solicitudes GET. La ruta de esta acción es la ruta
especificada en el controlador. No es necesario usar el nombre de acción como parte de la ruta. Solo debe
asegurarse de que cada acción tiene una ruta única e inequívoca. El enrutamiento de atributos se puede aplicar
tanto a los niveles de controlador como de método para crear rutas específicas.
[HttpGet]
public IActionResult List()
{
return Ok(_toDoRepository.All);
}

El método List devuelve un código de respuesta 200 OK y todos los elementos de lista de tareas, serializados
como JSON.
Puede probar el nuevo método de API con una variedad de herramientas, como Postman, que se muestra a
continuación:

Crear elementos
Por convención, la creación de elementos de datos se asigna al verbo HTTP POST. El método Create tiene un
atributo [HttpPost] aplicado y acepta una instancia ToDoItem . Puesto que el argumento item se pasará en el
cuerpo de la solicitud POST, este parámetro se decora con el atributo [FromBody] .
Dentro del método, se comprueba la validez del elemento y si existió anteriormente en el almacén de datos y, si no
hay problemas, se agrega mediante el repositorio. Al comprobar ModelState.IsValid se realiza una validación de
modelos, y debe realizarse en cada método de API que acepte datos proporcionados por usuario.
[HttpPost]
public IActionResult Create([FromBody] ToDoItem item)
{
try
{
if (item == null || !ModelState.IsValid)
{
return BadRequest(ErrorCode.TodoItemNameAndNotesRequired.ToString());
}
bool itemExists = _toDoRepository.DoesItemExist(item.ID);
if (itemExists)
{
return StatusCode(StatusCodes.Status409Conflict, ErrorCode.TodoItemIDInUse.ToString());
}
_toDoRepository.Insert(item);
}
catch (Exception)
{
return BadRequest(ErrorCode.CouldNotCreateItem.ToString());
}
return Ok(item);
}

El ejemplo usa una enumeración que contiene códigos de error que se pasan al cliente móvil:

public enum ErrorCode


{
TodoItemNameAndNotesRequired,
TodoItemIDInUse,
RecordNotFound,
CouldNotCreateItem,
CouldNotUpdateItem,
CouldNotDeleteItem
}

Pruebe a agregar nuevos elementos con Postman eligiendo el verbo POST que proporciona el nuevo objeto en
formato JSON en el cuerpo de la solicitud. También debe agregar un encabezado de solicitud que especifica un
Content-Type de application/json .
El método devuelve el elemento recién creado en la respuesta.
Actualizar elementos
La modificación de registros se realiza mediante solicitudes HTTP PUT. Aparte de este cambio, el método Edit es
casi idéntico a Create . Tenga en cuenta que, si no se encuentra el registro, la acción Edit devolverá una respuesta
NotFound (404 ).
[HttpPut]
public IActionResult Edit([FromBody] ToDoItem item)
{
try
{
if (item == null || !ModelState.IsValid)
{
return BadRequest(ErrorCode.TodoItemNameAndNotesRequired.ToString());
}
var existingItem = _toDoRepository.Find(item.ID);
if (existingItem == null)
{
return NotFound(ErrorCode.RecordNotFound.ToString());
}
_toDoRepository.Update(item);
}
catch (Exception)
{
return BadRequest(ErrorCode.CouldNotUpdateItem.ToString());
}
return NoContent();
}

Para probar con Postman, cambie el verbo a PUT. Especifique los datos actualizados del objeto en el cuerpo de la
solicitud.

Este método devuelve una respuesta NoContent (204) cuando se realiza correctamente, para mantener la
coherencia con la API existente.
Eliminar elementos
La eliminación de registros se consigue mediante solicitudes DELETE al servicio y pasando el identificador del
elemento que va a eliminar. Al igual que con las actualizaciones, las solicitudes para elementos que no existen
recibirán respuestas NotFound . De lo contrario, las solicitudes correctas recibirán una respuesta NoContent (204).

[HttpDelete("{id}")]
public IActionResult Delete(string id)
{
try
{
var item = _toDoRepository.Find(id);
if (item == null)
{
return NotFound(ErrorCode.RecordNotFound.ToString());
}
_toDoRepository.Delete(id);
}
catch (Exception)
{
return BadRequest(ErrorCode.CouldNotDeleteItem.ToString());
}
return NoContent();
}

Tenga en cuenta que, al probar la funcionalidad de eliminar, no se necesita nada en el cuerpo de la solicitud.

Convenciones comunes de Web API


Al desarrollar los servicios back-end de la aplicación, necesitará acceder a un conjunto coherente de convenciones o
directivas para controlar cuestiones transversales. Por ejemplo, en el servicio mostrado anteriormente, las
solicitudes de registros específicos que no se encontraron recibieron una respuesta NotFound , en lugar de una
respuesta BadRequest . De forma similar, los comandos realizados a este servicio que pasaron en tipos enlazados a
un modelo siempre se comprobaron como ModelState.IsValid y devolvieron una BadRequest para los tipos de
modelos no válidos.
Después de identificar una directiva común para las API, normalmente puede encapsularla en un filtro. Obtenga
más información sobre cómo encapsular directivas de API comunes en aplicaciones de ASP.NET Core MVC.
Páginas de ayuda de ASP.NET Core Web API con
Swagger/Open API
25/06/2018 • 5 minutes to read • Edit Online

Por Christoph Nienaber y Rico Suter


Cuando un desarrollador usa una API Web, puede que le resulte complicado comprender sus diversos métodos.
Swagger, también conocido como Open API, resuelve el problema de generar páginas útiles de ayuda y
documentación relativas a las API Web. Así, reporta ventajas como una documentación interactiva, la generación
de SDK de cliente y la detectabilidad de API.
En este artículo, nos centraremos en las implementaciones de Swagger .NET Swashbuckle.AspNetCore y
NSwag:
Swashbuckle.AspNetCore es un proyecto de código abierto para generar documentos de Swagger para
las API web de ASP.NET Core.
NSwag es otro proyecto de código abierto que sirve para integrar la interfaz de usuario de Swagger o
ReDoc en las API Web de ASP.NET Core. Ofrece métodos para generar código de cliente de C# y
TypeScript para la API.

¿Qué es Swagger/Open API?


Swagger es una especificación independiente del lenguaje que sirve para describir API de REST. El proyecto de
Swagger se donó a la iniciativa OpenAPI, donde se ahora conoce como Open API. Ambos nombres se usan
indistintamente, aunque se prefiere Open API. Gracias a esta API, tanto los equipos como los personas podrán
conocer las funciones de un servicio sin necesidad de obtener acceso directo a la implementación (código fuente,
acceso a la red, documentación). Uno de los objetivos consiste en reducir al mínimo la cantidad de trabajo
necesario para conectar servicios que no están asociados. Otro es reducir la cantidad de tiempo necesario para
documentar un servicio con precisión.

Especificación de Swagger (swagger.json)


El elemento principal del flujo de Swagger es la especificación de Swagger, que es de forma predeterminada un
documento llamado swagger.json. Lo genera la cadena de herramientas de Swagger (o las implementaciones de
terceros de dicha cadena) en función de su servicio. Describe las funciones de su API y cómo tener acceso a ella
con HTTP. Esta especificación también controla la interfaz de usuario de Swagger, y la cadena de herramientas la
usa para permitir la detección y generación de código de cliente. Este es un ejemplo de especificación de
Swagger (se ha reducido por motivos de brevedad):
{
"swagger": "2.0",
"info": {
"version": "v1",
"title": "API V1"
},
"basePath": "/",
"paths": {
"/api/Todo": {
"get": {
"tags": [
"Todo"
],
"operationId": "ApiTodoGet",
"consumes": [],
"produces": [
"text/plain",
"application/json",
"text/json"
],
"responses": {
"200": {
"description": "Success",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/TodoItem"
}
}
}
}
},
"post": {
...
}
},
"/api/Todo/{id}": {
"get": {
...
},
"put": {
...
},
"delete": {
...
},
"definitions": {
"TodoItem": {
"type": "object",
"properties": {
"id": {
"format": "int64",
"type": "integer"
},
"name": {
"type": "string"
},
"isComplete": {
"default": false,
"type": "boolean"
}
}
}
},
"securityDefinitions": {}
}
Interfaz de usuario de Swagger
La interfaz de usuario de Swagger es una interfaz de usuario basada en Internet que proporciona información
sobre el servicio por medio de la especificación de Swagger generada. Swashbuckle y NSwag incluyen una
versión insertada de la interfaz de usuario de Swagger, de modo que se puede hospedar en una aplicación
ASP.NET Core realizando una llamada de registro de middleware. La interfaz de usuario web tiene este aspecto:

Todos los métodos de acción públicos aplicados a los controladores se pueden probar desde la interfaz de
usuario. Haga clic en un nombre de método para expandir la sección. Agregue todos los parámetros necesarios y
haga clic en Try it out! (¡ Pruébelo!).
NOTE
La versión de interfaz de usuario de Swagger usada para las capturas de pantalla es la versión 2. Para obtener un ejemplo
de la versión 3, vea el ejemplo de Petstore.

Pasos siguientes
Get started with Swashbuckle (Introducción a Swashbuckle)
Get started with NSwag (Introducción a NSwag)
Trabajo con datos en ASP.NET Core
21/06/2018 • 2 minutes to read • Edit Online

Introducción a las páginas de Razor y Entity Framework Core con Visual Studio
Introducción a las páginas de Razor y EF
Operaciones de creación, lectura, actualización y eliminación
Ordenación, filtrado, paginación y agrupación
Migraciones
Creación de un modelo de datos complejo
Lectura de datos relacionados
Actualización de datos relacionados
Control de conflictos de simultaneidad
Introducción a ASP.NET Core MVC y Entity Framework Core con Visual Studio
Introducción
Operaciones de creación, lectura, actualización y eliminación
Ordenación, filtrado, paginación y agrupación
Migraciones
Creación de un modelo de datos complejo
Lectura de datos relacionados
Actualización de datos relacionados
Control de conflictos de simultaneidad
Herencia
Temas avanzados
ASP.NET Core con EF Core: nueva base de datos (sitio de la documentación de Entity Framework Core)
ASP.NET Core con EF Core: base de datos existente (sitio de la documentación de Entity Framework Core)
Introducción a ASP.NET Core y Entity Framework 6
Azure Storage
Agregar Azure Storage mediante el uso de Servicios conectados de Visual Studio
Introducción a Azure Blob Storage y Servicios conectados de Visual Studio
Introducción a Queue Storage y Servicios conectados de Visual Studio
Introducción a Azure Table Storage y Servicios conectados de Visual Studio
Páginas de Razor de ASP.NET Core con EF Core:
serie de tutoriales
21/06/2018 • 2 minutes to read • Edit Online

En esta serie de tutoriales aprenderá a crear aplicaciones web de páginas de Razor de ASP.NET Core que usen
Entity Framework (EF ) Core para acceder a los datos. Los tutoriales requieren Visual Studio 2017.
1. Introducción
2. Operaciones de creación, lectura, actualización y eliminación
3. Ordenado, filtrado, paginación y agrupación
4. Migraciones
5. Creación de un modelo de datos complejo
6. Lectura de datos relacionados
7. Actualización de datos relacionados
8. Control de conflictos de simultaneidad
Páginas de Razor con Entity Framework Core en
ASP.NET Core: Tutorial 1 de 8
25/06/2018 • 32 minutes to read • Edit Online

Por Tom Dykstra y Rick Anderson


En la aplicación web de ejemplo Contoso University se muestra cómo crear aplicaciones web ASP.NET Core 2.0
MVC con Entity Framework (EF ) Core 2.0 y Visual Studio 2017.
La aplicación de ejemplo es un sitio web de una universidad ficticia, Contoso University. Incluye funciones como la
admisión de estudiantes, la creación de cursos y asignaciones de instructores. Esta página es la primera de una
serie de tutoriales en los que se explica cómo crear la aplicación de ejemplo Contoso University.
Descargue o vea la aplicación completa. Instrucciones de descarga.

Requisitos previos
Install one of the following:
CLI tooling: Windows, Linux, or macOS: .NET Core SDK 2.0 or later
IDE/editor tooling
Windows: Visual Studio for Windows
ASP.NET and web development workload
.NET Core cross-platform development workload
Linux: Visual Studio Code
macOS: Visual Studio for Mac
Familiaridad con las Páginas de Razor. Los programadores nuevos deben completar Introducción a las páginas de
Razor en ASP.NET Core antes de empezar esta serie.

Solución de problemas
Si experimenta un problema que no puede resolver, por lo general podrá encontrar la solución si compara el código
con la fase completada. Para obtener una lista de errores comunes y cómo resolverlos, vea la sección de solución de
problemas del último tutorial de la serie. Si ahí no encuentra lo que necesita, puede publicar una pregunta en
StackOverflow.com para ASP.NET Core o EF Core.

TIP
Esta serie de tutoriales se basa en lo que se realiza en los tutoriales anteriores. Considere la posibilidad de guardar una copia
del proyecto después de completar correctamente cada tutorial. Si experimenta problemas, puede empezar desde el tutorial
anterior en lugar de volver al principio. Como alternativa, puede descargar una fase completada y empezar de nuevo con la
fase completada.

La aplicación web Contoso University


La aplicación compilada en estos tutoriales es un sitio web básico de una universidad.
Los usuarios pueden ver y actualizar la información de estudiantes, cursos e instructores. Estas son algunas de las
pantallas que se crean en el tutorial.
El estilo de la interfaz de usuario de este sitio se mantiene fiel a lo que generan las plantillas integradas. El tutorial
se centra en EF Core con páginas de Razor, no en la interfaz de usuario.

Creación de una aplicación web de páginas de Razor


En el menú Archivo de Visual Studio, seleccione Nuevo > Proyecto.
Cree una aplicación web de ASP.NET Core. Asigne el nombre ContosoUniversity al proyecto. Es importante
que el nombre del proyecto sea ContosoUniversity para que coincidan los espacios de nombres al copiar y
pegar el código.

Seleccione ASP.NET Core 2.0 en la lista desplegable y, luego, seleccione Aplicación web.
Presione F5 para ejecutar la aplicación en modo de depuración o Ctrl-F5 para que se ejecute sin adjuntar el
depurador.

Configurar el estilo del sitio


Con algunos cambios se configura el menú del sitio, el diseño y la página principal.
Abra Pages/_Layout.cshtml y realice los cambios siguientes:
Cambie todas las repeticiones de "ContosoUniversity" por "Contoso University". Hay tres repeticiones.
Agregue entradas de menú para Students, Courses, Instructors y Departments, y elimine la entrada de
menú Contact.
Los cambios aparecen resaltados. (No se muestra todo el marcado).
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - Contoso University</title>

<environment include="Development">
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
<link rel="stylesheet" href="~/css/site.css" />
</environment>
<environment exclude="Development">
<link rel="stylesheet" href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.min.css"
asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-
value="absolute" />
<link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" />
</environment>
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-
collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a asp-page="/Index" class="navbar-brand">Contoso University</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a asp-page="/Index">Home</a></li>
<li><a asp-page="/About">About</a></li>
<li><a asp-page="/Students/Index">Students</a></li>
<li><a asp-page="/Courses/Index">Courses</a></li>
<li><a asp-page="/Instructors/Index">Instructors</a></li>
<li><a asp-page="/Departments/Index">Departments</a></li>
</ul>
</div>
</div>
</nav>
<div class="container body-content">
@RenderBody()
<hr />
<footer>
<p>&copy; 2017 - Contoso University</p>
</footer>
</div>

En Pages/Index.cshtml, reemplace el contenido del archivo con el código siguiente para reemplazar el texto sobre
ASP.NET y MVC con texto sobre esta aplicación:
@page
@model IndexModel
@{
ViewData["Title"] = "Home page";
}

<div class="jumbotron">
<h1>Contoso University</h1>
</div>
<div class="row">
<div class="col-md-4">
<h2>Welcome to Contoso University</h2>
<p>
Contoso University is a sample application that
demonstrates how to use Entity Framework Core in an
ASP.NET Core Razor Pages web app.
</p>
</div>
<div class="col-md-4">
<h2>Build it from scratch</h2>
<p>You can build the application by following the steps in a series of tutorials.</p>
<p><a class="btn btn-default"
href="https://docs.microsoft.com/aspnet/core/data/ef-rp/intro">
See the tutorial &raquo;</a></p>
</div>
<div class="col-md-4">
<h2>Download it</h2>
<p>You can download the completed project from GitHub.</p>
<p><a class="btn btn-default"
href="https://github.com/aspnet/Docs/tree/master/aspnetcore/data/ef-rp/intro/samples/cu-final">
See project source code &raquo;</a></p>
</div>
</div>

Presione CTRL+F5 para ejecutar el proyecto. La página principal se muestra con las pestañas creadas en los
tutoriales siguientes:
Crear el modelo de datos
Cree las clases de entidad para la aplicación Contoso University. Comience con las tres entidades siguientes:

Hay una relación uno a varios entre las entidades Student y Enrollment . Hay una relación uno a varios entre las
entidades Course y Enrollment . Un estudiante se puede inscribir en cualquier número de cursos. Un curso puede
tener cualquier número de alumnos inscritos.
En las secciones siguientes, se crea una clase para cada una de estas entidades.
La entidad Student

Cree una carpeta Models. En la carpeta Models, cree un archivo de clase denominado Student.cs con el código
siguiente:

using System;
using System.Collections.Generic;

namespace ContosoUniversity.Models
{
public class Student
{
public int ID { get; set; }
public string LastName { get; set; }
public string FirstMidName { get; set; }
public DateTime EnrollmentDate { get; set; }

public ICollection<Enrollment> Enrollments { get; set; }


}
}

La propiedad ID se convierte en la columna de clave principal de la tabla de base de datos (DB ) que corresponde a
esta clase. De forma predeterminada, EF Core interpreta como la clave principal una propiedad que se denomine
ID o classnameID . En classnameID , classname es el nombre de la clase (como Student en el ejemplo anterior ).

La propiedad Enrollments es una propiedad de navegación. Las propiedades de navegación se vinculan a otras
entidades relacionadas con esta entidad. En este caso, la propiedad Enrollments de una Student entity contiene
todas las entidades Enrollment que están relacionadas con esa entidad Student . Por ejemplo, si una fila Student
de la base de datos tiene dos filas Enrollment relacionadas, la propiedad de navegación Enrollments contiene esas
dos entidades Enrollment . Una fila Enrollment relacionada es la que contiene el valor de clave principal de ese
estudiante en la columna StudentID . Por ejemplo, suponga que el estudiante con ID=1 tiene dos filas en la tabla
Enrollment . La tabla Enrollment tiene dos filas con StudentID = 1. StudentID es una clave externa en la tabla
Enrollment que especifica el estudiante en la tabla Student .

Si una propiedad de navegación puede contener varias entidades, la propiedad de navegación debe ser un tipo de
lista, como ICollection<T> . Se puede especificar ICollection<T> , o bien un tipo como List<T> o HashSet<T> .
Cuando se usa ICollection<T> , EF Core crea una colección HashSet<T> de forma predeterminada. Las propiedades
de navegación que contienen varias entidades proceden de relaciones de varios a varios y uno a varios.
La entidad Enrollment
En la carpeta Models, cree Enrollment.cs con el código siguiente:

namespace ContosoUniversity.Models
{
public enum Grade
{
A, B, C, D, F
}

public class Enrollment


{
public int EnrollmentID { get; set; }
public int CourseID { get; set; }
public int StudentID { get; set; }
public Grade? Grade { get; set; }

public Course Course { get; set; }


public Student Student { get; set; }
}
}

La propiedad EnrollmentID es la clave principal. En esta entidad se usa el patrón classnameID en lugar de ID
como en la entidad Student . Normalmente, los desarrolladores eligen un patrón y lo usan en todo el modelo de
datos. En un tutorial posterior, se muestra el uso de ID sin un nombre de clase para facilitar la implementación de la
herencia en el modelo de datos.
La propiedad Grade es una enum . El signo de interrogación después de la declaración de tipo Grade indica que la
propiedad Grade acepta valores NULL. Una calificación que sea NULL es diferente de una calificación que sea
cero; NULL significa que no se conoce una calificación o que todavía no se ha asignado.
La propiedad StudentID es una clave externa y la propiedad de navegación correspondiente es Student . Una
entidad Enrollment está asociada con una entidad Student , por lo que la propiedad contiene una única entidad
Student . La entidad Student difiere de la propiedad de navegación Student.Enrollments , que contiene varias
entidades Enrollment .
La propiedad CourseID es una clave externa y la propiedad de navegación correspondiente es Course . Una
entidad Enrollment está asociada con una entidad Course .
EF Core interpreta una propiedad como una clave externa si se denomina
<navigation property name><primary key property name> . Por ejemplo, StudentID para la propiedad de navegación
Student , puesto que la clave principal de la entidad Student es ID . Las propiedades de clave externa también se
pueden denominar <primary key property name> . Por ejemplo CourseID , dado que la clave principal de la entidad
Course es CourseID .

La entidad Course
En la carpeta Models, cree Course.cs con el código siguiente:

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
public class Course
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int CourseID { get; set; }
public string Title { get; set; }
public int Credits { get; set; }

public ICollection<Enrollment> Enrollments { get; set; }


}
}

La propiedad Enrollments es una propiedad de navegación. Una entidad Course puede estar relacionada con
cualquier número de entidades Enrollment .
El atributo DatabaseGenerated permite que la aplicación especifique la clave principal en lugar de hacer que la base
de datos la genere.

Crear el contexto de base de datos SchoolContext


La clase principal que coordina la funcionalidad de EF Core para un modelo de datos determinado es la clase de
contexto de base de datos. El contexto de datos se deriva de Microsoft.EntityFrameworkCore.DbContext . En el
contexto de datos se especifica qué entidades se incluyen en el modelo de datos. En este proyecto, la clase se
denomina SchoolContext .
En la carpeta del proyecto, cree una carpeta denominada Data.
En la carpeta Data, cree SchoolContext.cs con el código siguiente:

using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity.Data
{
public class SchoolContext : DbContext
{
public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
{
}

public DbSet<Course> Courses { get; set; }


public DbSet<Enrollment> Enrollments { get; set; }
public DbSet<Student> Students { get; set; }
}
}
Este código crea una propiedad DbSet para cada conjunto de entidades. En la terminología de EF Core:
Un conjunto de entidades normalmente se corresponde a una tabla de base de datos.
Una entidad se corresponde con una fila de la tabla.
DbSet<Enrollment> y DbSet<Course> se pueden omitir. EF Core las incluye implícitamente porque la entidad
Student hace referencia a la entidad Enrollment y la entidad Enrollment hace referencia a la entidad Course . Para
este tutorial, conserve DbSet<Enrollment> y DbSet<Course> en el SchoolContext .
Cuando se crea la base de datos, EF Core crea las tablas con los mismos nombres que los nombres de propiedad
DbSet . Los nombres de propiedad para las colecciones normalmente están en plural ( Students en lugar de
Student). Los desarrolladores están en desacuerdo sobre si los nombres de tabla deben estar en plural. Para estos
tutoriales, se invalida el comportamiento predeterminado mediante la especificación de nombres de tabla en
singular en DbContext. Para especificar los nombres de tabla en singular, agregue el código resaltado siguiente:

using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity.Data
{
public class SchoolContext : DbContext
{
public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
{
}

public DbSet<Course> Courses { get; set; }


public DbSet<Enrollment> Enrollments { get; set; }
public DbSet<Student> Students { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)


{
modelBuilder.Entity<Course>().ToTable("Course");
modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
modelBuilder.Entity<Student>().ToTable("Student");
}
}
}

Registro del contexto con inserción de dependencias


ASP.NET Core incluye la inserción de dependencias. Los servicios (como el contexto de base de datos de EF Core)
se registran con inserción de dependencias durante el inicio de la aplicación. Estos servicios se proporcionan a los
componentes que los necesitan (como las páginas de Razor) a través de parámetros de constructor. El código de
constructor que obtiene una instancia de contexto de base de datos se muestra más adelante en el tutorial.
Para registrar SchoolContext como servicio, abra Startup.cs y agregue las líneas resaltadas al método
ConfigureServices .

public void ConfigureServices(IServiceCollection services)


{
services.AddDbContext<SchoolContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

services.AddMvc();
}

El nombre de la cadena de conexión se pasa al contexto mediante una llamada a un método en un objeto
DbContextOptionsBuilder . Para el desarrollo local, el sistema de configuración de ASP.NET Core lee la cadena de
conexión desde el archivo appsettings.json.
Agregue instrucciones usingpara los espacios de nombres ContosoUniversity.Data y
Microsoft.EntityFrameworkCore . Compile el proyecto.

using ContosoUniversity.Data;
using Microsoft.EntityFrameworkCore;

Abra el archivo appsettings.json y agregue una cadena de conexión como se muestra en el código siguiente:

{
"ConnectionStrings": {
"DefaultConnection": "Server=
(localdb)\\mssqllocaldb;Database=ContosoUniversity1;ConnectRetryCount=0;Trusted_Connection=True;MultipleActiveR
esultSets=true"
},
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Warning"
}
}
}

En la cadena de conexión anterior se usa ConnectRetryCount=0 para evitar que SQLClient se bloquee.
SQL Server Express LocalDB
La cadena de conexión especifica una base de datos de SQL Server LocalDB. LocalDB es una versión ligera del
motor de base de datos de SQL Server Express y está dirigida al desarrollo de aplicaciones, no al uso en
producción. LocalDB se inicia a petición y se ejecuta en modo de usuario, sin necesidad de una configuración
compleja. De forma predeterminada, LocalDB crea archivos de base de datos .mdf en el directorio C:/Users/<user> .

Agregar código para inicializar la base de datos con datos de prueba


EF Core crea una base de datos vacía. En esta sección, se escribe un método Seed para rellenarla con datos de
prueba.
En la carpeta Data, cree un archivo de clase denominado DbInitializer.cs y agregue el código siguiente:

using ContosoUniversity.Models;
using System;
using System.Linq;

namespace ContosoUniversity.Data
{
public static class DbInitializer
{
public static void Initialize(SchoolContext context)
{
context.Database.EnsureCreated();

// Look for any students.


if (context.Students.Any())
{
return; // DB has been seeded
}

var students = new Student[]


{
new Student{FirstMidName="Carson",LastName="Alexander",EnrollmentDate=DateTime.Parse("2005-09-
01")},
01")},
new Student{FirstMidName="Meredith",LastName="Alonso",EnrollmentDate=DateTime.Parse("2002-09-01")},
new Student{FirstMidName="Arturo",LastName="Anand",EnrollmentDate=DateTime.Parse("2003-09-01")},
new Student{FirstMidName="Gytis",LastName="Barzdukas",EnrollmentDate=DateTime.Parse("2002-09-01")},
new Student{FirstMidName="Yan",LastName="Li",EnrollmentDate=DateTime.Parse("2002-09-01")},
new Student{FirstMidName="Peggy",LastName="Justice",EnrollmentDate=DateTime.Parse("2001-09-01")},
new Student{FirstMidName="Laura",LastName="Norman",EnrollmentDate=DateTime.Parse("2003-09-01")},
new Student{FirstMidName="Nino",LastName="Olivetto",EnrollmentDate=DateTime.Parse("2005-09-01")}
};
foreach (Student s in students)
{
context.Students.Add(s);
}
context.SaveChanges();

var courses = new Course[]


{
new Course{CourseID=1050,Title="Chemistry",Credits=3},
new Course{CourseID=4022,Title="Microeconomics",Credits=3},
new Course{CourseID=4041,Title="Macroeconomics",Credits=3},
new Course{CourseID=1045,Title="Calculus",Credits=4},
new Course{CourseID=3141,Title="Trigonometry",Credits=4},
new Course{CourseID=2021,Title="Composition",Credits=3},
new Course{CourseID=2042,Title="Literature",Credits=4}
};
foreach (Course c in courses)
{
context.Courses.Add(c);
}
context.SaveChanges();

var enrollments = new Enrollment[]


{
new Enrollment{StudentID=1,CourseID=1050,Grade=Grade.A},
new Enrollment{StudentID=1,CourseID=4022,Grade=Grade.C},
new Enrollment{StudentID=1,CourseID=4041,Grade=Grade.B},
new Enrollment{StudentID=2,CourseID=1045,Grade=Grade.B},
new Enrollment{StudentID=2,CourseID=3141,Grade=Grade.F},
new Enrollment{StudentID=2,CourseID=2021,Grade=Grade.F},
new Enrollment{StudentID=3,CourseID=1050},
new Enrollment{StudentID=4,CourseID=1050},
new Enrollment{StudentID=4,CourseID=4022,Grade=Grade.F},
new Enrollment{StudentID=5,CourseID=4041,Grade=Grade.C},
new Enrollment{StudentID=6,CourseID=1045},
new Enrollment{StudentID=7,CourseID=3141,Grade=Grade.A},
};
foreach (Enrollment e in enrollments)
{
context.Enrollments.Add(e);
}
context.SaveChanges();
}
}
}

El código comprueba si hay estudiantes en la base de datos. Si no hay ningún estudiante en la base de datos, se
inicializa con datos de prueba. Carga los datos de prueba en matrices en lugar de colecciones List<T> para
optimizar el rendimiento.
El método EnsureCreated crea automáticamente la base de datos para el contexto de base de datos. Si la base de
datos existe, EnsureCreated vuelve sin modificarla.
En Program.cs, modifique el método Main para que haga lo siguiente:
Obtener una instancia del contexto de base de datos desde el contenedor de inserción de dependencias.
Llamar al método de inicialización, pasándolo al contexto.
Eliminar el contexto cuando el método de inicialización finalice.
En el código siguiente se muestra el archivo Program.cs actualizado.

// Unused usings removed


using System;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.DependencyInjection;
using ContosoUniversity.Data;

namespace ContosoUniversity
{
public class Program
{
public static void Main(string[] args)
{
var host = BuildWebHost(args);

using (var scope = host.Services.CreateScope())


{
var services = scope.ServiceProvider;
try
{
var context = services.GetRequiredService<SchoolContext>();
DbInitializer.Initialize(context);
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred while seeding the database.");
}
}

host.Run();
}

public static IWebHost BuildWebHost(string[] args) =>


WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.Build();
}
}

La primera vez que se ejecuta la aplicación, se crea la base de datos y se inicializa con datos de prueba. Cuando se
actualice el modelo de datos:
Se elimina la base de datos.
Se actualiza el método de inicialización.
Se ejecuta la aplicación y se crea una base de datos inicializada.
En los tutoriales posteriores, la base de datos se actualiza cuando cambia el modelo de datos, sin tener que
eliminarla y volver a crearla.

Agregar herramientas de scaffolding


En esta sección, se usa la Consola del Administrador de paquetes (PMC ) para agregar el paquete de generación de
código web de Visual Studio. Este paquete es necesario para ejecutar el motor de scaffolding.
En el menú Herramientas, seleccione Administrador de paquetes NuGet > Consola del Administrador de
paquetes.
En la Consola del Administrador de paquetes (PMC ), escriba los comandos siguientes:

Install-Package Microsoft.VisualStudio.Web.CodeGeneration.Design
Install-Package Microsoft.VisualStudio.Web.CodeGeneration.Utils

El comando anterior agrega los paquetes NuGet al archivo *.csproj:

<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Utils" Version="2.0.0" />
</ItemGroup>
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.0" />
</ItemGroup>
</Project>

Aplicar scaffolding al modelo


Abra una ventana de comandos en el directorio del proyecto (el directorio que contiene los archivos Program.cs,
Startup.cs y .csproj).
Ejecute los comandos siguientes:

dotnet restore
dotnet tool install --global dotnet-aspnet-codegenerator --version 2.1.0
dotnet aspnet-codegenerator razorpage -m Student -dc SchoolContext -udl -outDir Pages\Students --
referenceScriptLibraries

Si se produce un error:

No executable found matching command "dotnet-aspnet-codegenerator"

Abra una ventana de comandos en el directorio del proyecto (el directorio que contiene los archivos Program.cs,
Startup.cs y .csproj).
Compile el proyecto. La compilación genera errores similares a los siguientes:
1>Pages\Students\Index.cshtml.cs(26,38,26,45): error CS1061: 'SchoolContext' does not contain a definition for
'Student'

Cambie globalmente _context.Student por _context.Students (es decir, agregue una "s" a Student ). Se
encuentran y actualizan siete repeticiones. Esperamos solucionar este problema en la próxima versión.
En la tabla siguiente se incluyen los detalles de los parámetros de los generadores de código de ASP.NET Core:

PARÁMETRO DESCRIPTION

-m Nombre del modelo

-dc Contexto de datos

-udl Usa el diseño predeterminado.


PARÁMETRO DESCRIPTION

-outDir Ruta de acceso relativa de la carpeta de salida para crear las


vistas

--referenceScriptLibraries Agrega _ValidationScriptsPartial a las páginas Editar y


Crear.

Active o desactive h para obtener ayuda con el comando aspnet-codegenerator razorpage :

dotnet aspnet-codegenerator razorpage -h

Prueba de la aplicación
Ejecute la aplicación y haga clic en el vínculo Students. Según el ancho del explorador, el vínculo Students aparece
en la parte superior de la página. Si el vínculo Students no se ve, haga clic en el icono de navegación en la esquina
superior derecha.

Pruebe los vínculos Create, Edit y Details.

Ver la base de datos


Cuando se inicia la aplicación, DbInitializer.Initialize llama a EnsureCreated . EnsureCreated detecta si la base
de datos existe y crea una si es necesario. Si no hay ningún estudiante en la base de datos, el método Initialize
los agrega.
Abra el Explorador de objetos de SQL Server (SSOX) desde el menú Vista en Visual Studio. En SSOX, haga clic
en (localdb)\MSSQLLocalDB > Databases > ContosoUniversity1.
Expanda el nodo Tablas.
Haga clic con el botón derecho en la tabla Student y haga clic en Ver datos para ver las columnas que se crearon y
las filas que se insertaron en la tabla.
Los archivos de base de datos .mdf y .ldf se encuentran en la carpeta C:\Usuarios\.
EnsureCreated se llama durante el inicio de la aplicación, lo que permite el flujo de trabajo siguiente:
Se elimina la base de datos.
Se cambia el esquema de base de datos (por ejemplo, se agrega un campo EmailAddress ).
Ejecute la aplicación.
EnsureCreated crea una base de datos con la columna EmailAddress .

Convenciones
La cantidad de código que se escribe para que EF Core cree una base de datos completa es mínima debido al uso
de convenciones o a las suposiciones que hace EF Core.
Los nombres de las propiedades DbSet se usan como nombres de tabla. Para las entidades a las que no se
hace referencia con una propiedad DbSet , los nombres de clase de entidad se usan como nombres de tabla.
Los nombres de propiedad de entidad se usan para los nombres de columna.
Las propiedades de entidad que se denominan ID o classnameID se reconocen como propiedades de clave
principal.
Una propiedad se interpreta como propiedad de clave externa si se denomina (por ejemplo, StudentID para
la propiedad de navegación Student , dado que la clave principal de la entidad Student es ID ). Las
propiedades de clave externa también se pueden denominar (por ejemplo EnrollmentID , dado que la clave
principal de la entidad Enrollment es EnrollmentID ).
El comportamiento de las convenciones se puede reemplazar. Por ejemplo, los nombres de tabla se pueden
especificar explícitamente, como se muestra anteriormente en este tutorial. Los nombres de columna se pueden
establecer explícitamente. Las claves principales y las claves externas se pueden establecer explícitamente.

Código asincrónico
La programación asincrónica es el modo predeterminado de ASP.NET Core y EF Core.
Un servidor web tiene un número limitado de subprocesos disponibles y, en situaciones de carga alta, es posible
que todos los subprocesos disponibles estén en uso. Cuando esto ocurre, el servidor no puede procesar nuevas
solicitudes hasta que los subprocesos se liberen. Con el código sincrónico, se pueden acumular muchos
subprocesos mientras no estén realizando ningún trabajo porque están a la espera de que finalice la E/S. Con el
código asincrónico, cuando un proceso está a la espera de que finalice la E/S, se libera su subproceso para el que el
servidor lo use para el procesamiento de otras solicitudes. Como resultado, el código asincrónico permite que los
recursos de servidor se usen de forma más eficaz, y el servidor está habilitado para administrar más tráfico sin
retrasos.
El código asincrónico introduce una pequeña cantidad de sobrecarga en tiempo de ejecución. En situaciones de
poco tráfico, la disminución del rendimiento es insignificante, mientras que en situaciones de tráfico elevado, la
posible mejora del rendimiento es importante.
En el código siguiente, la palabra clave async , el valor devuelto Task<T> , la palabra clave await y el método
ToListAsync hacen que el código se ejecute de forma asincrónica.

public async Task OnGetAsync()


{
Student = await _context.Students.ToListAsync();
}

La palabra clave async indica al compilador que:


Genere devoluciones de llamada para partes del cuerpo del método.
Cree automáticamente el objeto Task que se devuelve. Para más información, vea Tipo de valor devuelto
Task.
El tipo devuelto implícito Task representa el trabajo en curso.
La palabra clave await hace que el compilador divida el método en dos partes. La primera parte termina
con la operación que se inició de forma asincrónica. La segunda parte se coloca en un método de devolución
de llamada que se llama cuando finaliza la operación.
ToListAsync es la versión asincrónica del método de extensión ToList .
Algunos aspectos que tener en cuenta al escribir código asincrónico en el que se usa EF Core son los siguientes:
Solo se ejecutan de forma asincrónica las instrucciones que hacen que las consultas o los comandos se
envíen a la base de datos. Esto incluye ToListAsync , SingleOrDefaultAsync , FirstOrDefaultAsync y
SaveChangesAsync . No incluye las instrucciones que solo cambian una IQueryable , como
var students = context.Students.Where(s => s.LastName == "Davolio") .

Un contexto de EF Core no es seguro para subprocesos: no intente realizar varias operaciones en paralelo.
Para aprovechar las ventajas de rendimiento del código asincrónico, compruebe que en los paquetes de
biblioteca (por ejemplo para paginación) se usa async si llaman a métodos de EF Core que envían consultas
a la base de datos.
Para obtener más información sobre la programación asincrónica en .NET, vea Información general de Async.
En el siguiente tutorial, se examinan las operaciones CRUD (crear, leer, actualizar y eliminar) básicas.

S IG U IE N T E
Páginas de Razor con EF Core en ASP.NET Core:
CRUD (2 de 8)
25/06/2018 • 21 minutes to read • Edit Online

Por Tom Dykstra, Jon P Smith y Rick Anderson


La aplicación web Contoso University muestra cómo crear aplicaciones web de las páginas de Razor con EF Core
y Visual Studio. Para obtener información sobre la serie de tutoriales, consulte el primer tutorial.
En este tutorial, se revisa y personaliza el código CRUD (crear, leer, actualizar y eliminar) con scaffolding.
Nota: Para minimizar la complejidad y mantener estos tutoriales centrados en EF Core, en los modelos de página
de las páginas de Razor se usa código de EF Core. Algunos desarrolladores usan un patrón de capa o repositorio
de servicio para crear una capa de abstracción entre la interfaz de usuario (las páginas de Razor) y la capa de
acceso a datos.
En este tutorial, se modifican las páginas de Razor Create, Edit, Delete y Details de la carpeta Student.
En el código con scaffolding se usa el modelo siguiente para las páginas Create, Edit y Delete:
Obtenga y muestre los datos solicitados con el método HTTP GET OnGetAsync .
Guarde los cambios en los datos con el método HTTP POST OnPostAsync .
Las páginas Index y Details obtienen y muestran los datos solicitados con el método HTTP GET OnGetAsync

Reemplazar SingleOrDefaultAsync por FirstOrDefaultAsync


En el código generado se usa SingleOrDefaultAsync para capturar la entidad solicitada. FirstOrDefaultAsync es
más eficaz para capturar una entidad:
A menos que el código necesite comprobar que no hay más de una entidad devuelta por la consulta.
SingleOrDefaultAsync captura más datos y realiza trabajo innecesario.
SingleOrDefaultAsync inicia una excepción si hay más de una entidad que se ajuste a la parte del filtro.
FirstOrDefaultAsync no inicia una excepción si hay más de una entidad que se ajuste a la parte del filtro.

Reemplace globalmente SingleOrDefaultAsync con FirstOrDefaultAsync . SingleOrDefaultAsync se usa en cinco


lugares:
OnGetAsync en la página Details.
OnGetAsync y OnPostAsync en las páginas Edit y Delete.
FindAsync
En gran parte del código con scaffolding, se puede usar FindAsync en lugar de FirstOrDefaultAsync o
SingleOrDefaultAsync .

FindAsync :
Busca una entidad con la clave principal (PK). Si el contexto realiza el seguimiento de una entidad con la clave
principal, se devuelve sin una solicitud a la base de datos.
Es sencillo y conciso.
Está optimizado para buscar una sola entidad.
Puede tener ventajas de rendimiento en algunas situaciones, pero rara vez entra en juego para escenarios web
normales.
Usa implícitamente FirstAsync en lugar de SingleAsync. Pero si quiere incluir otras entidades, Find ya no
resulta apropiado. Esto significa que puede que necesite descartar Find y cambiar a una consulta cuando la
aplicación progrese.

Personalizar la página de detalles


Vaya a la página Pages/Students . Los vínculos Edit, Details y Delete son generados por la Aplicación auxiliar de
etiquetas delimitadoras del archivo Pages/Students/Index.cshtml.

<td>
<a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
</td>

Haga clic en un vínculo Details. La dirección URL tiene el formato http://localhost:5000/Students/Details?id=2 .


Se pasa Student ID mediante una cadena de consulta ( ?id=2 ).
Actualice las páginas de Razor Edit, Details y Delete para usar la plantilla de ruta "{id:int}" . Cambie la directiva
de página de cada una de estas páginas de @page a @page "{id:int}" .
Una solicitud a la página con la plantilla de ruta "{id:int}" que no incluya un valor de ruta entero devolverá un
error HTTP 404 (no encontrado). Por ejemplo, http://localhost:5000/Students/Details devuelve un error 404.
Para que el identificador sea opcional, anexe ? a la restricción de ruta:

@page "{id:int?}"

Ejecute la aplicación, haga clic en un vínculo Details y compruebe que la dirección URL pasa el identificador como
datos de ruta ( http://localhost:5000/Students/Details/2 ).
No cambie globalmente @page por @page "{id:int}" ; esta acción rompería los vínculos a las páginas Home y
Create.
Agregar datos relacionados
El código con scaffolding de la página Students Index no incluye la propiedad Enrollments . En esta sección, se
mostrará el contenido de la colección Enrollments en la página Details.
El método OnGetAsync de Pages/Students/Details.cshtml.cs usa el método FirstOrDefaultAsync para recuperar
una única entidad Student . Agregue el código resaltado siguiente:
public async Task<IActionResult> OnGetAsync(int? id)
{
if (id == null)
{
return NotFound();
}

Student = await _context.Students


.Include(s => s.Enrollments)
.ThenInclude(e => e.Course)
.AsNoTracking()
.FirstOrDefaultAsync (m => m.ID == id);

if (Student == null)
{
return NotFound();
}
return Page();
}

Los métodos y ThenInclude hacen que el contexto cargue la propiedad de navegación


Include
Student.Enrollments y, dentro de cada inscripción, la propiedad de navegación Enrollment.Course . Estos métodos
se examinan con detalle en el tutorial de lectura de datos relacionados.
El método AsNoTracking mejora el rendimiento en casos en los que las entidades devueltas no se actualizan en el
contexto actual. AsNoTracking se describe posteriormente en este tutorial.
Mostrar las inscripciones relacionadas en la página Details
Abra Pages/Students/Details.cshtml. Agregue el siguiente código resaltado para mostrar una lista de las
inscripciones:
[!code-cshtml]
Si la sangría de código no es correcta después de pegar el código, presione CTRL -K-D para corregirlo.
El código anterior recorre en bucle las entidades de la propiedad de navegación Enrollments . Para cada
inscripción, se muestra el título del curso y la calificación. El título del curso se recupera de la entidad Course
almacenada en la propiedad de navegación Course de la entidad Enrollments.
Ejecute la aplicación, haga clic en la pestaña Students y después en el vínculo Details de un estudiante. Se
muestra la lista de cursos y calificaciones para el alumno seleccionado.

Actualizar la página Create


Actualice el método OnPostAsync de Pages/Students/Create.cshtml.cs con el código siguiente:
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}

var emptyStudent = new Student();

if (await TryUpdateModelAsync<Student>(
emptyStudent,
"student", // Prefix for form value.
s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
{
_context.Students.Add(emptyStudent);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}

return null;
}

TryUpdateModelAsync
Examine el código de TryUpdateModelAsync:

var emptyStudent = new Student();

if (await TryUpdateModelAsync<Student>(
emptyStudent,
"student", // Prefix for form value.
s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
{

En el código anterior, TryUpdateModelAsync<Student> intenta actualizar el objeto emptyStudent mediante los


valores de formulario enviados desde la propiedad PageContext del PageModel. TryUpdateModelAsync solo
actualiza las propiedades enumeradas ( s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate ).
En el ejemplo anterior:
El segundo argumento ( "student", // Prefix ) es el prefijo que se usa para buscar valores. No distingue
mayúsculas de minúsculas.
Los valores de formulario enviados se convierten a los tipos del modelo Student mediante el enlace de
modelos.
Publicación excesiva
El uso de TryUpdateModel para actualizar campos con valores enviados es un procedimiento recomendado de
seguridad porque evita la publicación excesiva. Por ejemplo, suponga que la entidad Student incluye una
propiedad Secret que esta página web no debe actualizar ni agregar:

public class Student


{
public int ID { get; set; }
public string LastName { get; set; }
public string FirstMidName { get; set; }
public DateTime EnrollmentDate { get; set; }
public string Secret { get; set; }
}
Incluso si la aplicación no tiene un campo Secret en la de página de Razor de creación o actualización, un hacker
podría establecer el valor de Secret mediante publicación excesiva. Un hacker podría usar una herramienta
como Fiddler, o bien escribir código de JavaScript, para publicar un valor de formulario Secret . El código original
no limita los campos que el enlazador de modelos usa cuando crea una instancia Student.
El valor que haya especificado el hacker para el campo de formulario Secret se actualiza en la base de datos. En
la imagen siguiente se muestra cómo la herramienta Fiddler agrega el campo Secret (con el valor "OverPost") a
los valores de formulario enviados.

El valor "OverPost" se ha agregado correctamente a la propiedad Secret de la fila insertada. El diseñador de


aplicaciones no había previsto que la propiedad Secret se estableciera con la página Create.
Modelo de vista
Normalmente, un modelo de vista contiene un subconjunto de las propiedades incluidas en el modelo que usa la
aplicación. El modelo de aplicación se suele denominar modelo de dominio. El modelo de dominio normalmente
contiene todas las propiedades requeridas por la entidad correspondiente en la base de datos. El modelo de vista
contiene solo las propiedades necesarias para la capa de interfaz de usuario (por ejemplo, la página Create).
Además del modelo de vista, en algunas aplicaciones se usa un modelo de enlace o de entrada para pasar datos
entre la clase del modelo de página de las páginas de Razor y el explorador. Tenga en cuenta el modelo de vista
Student siguiente:

using System;

namespace ContosoUniversity.Models
{
public class StudentVM
{
public int ID { get; set; }
public string LastName { get; set; }
public string FirstMidName { get; set; }
public DateTime EnrollmentDate { get; set; }
}
}

Los modelos de vista ofrecen una forma alternativa de evitar la publicación excesiva. El modelo de vista contiene
solo las propiedades que se van a ver (mostrar) o actualizar.
En el código siguiente se usa el modelo de vista StudentVM para crear un alumno:

[BindProperty]
public StudentVM StudentVM { get; set; }

public async Task<IActionResult> OnPostAsync()


{
if (!ModelState.IsValid)
{
return Page();
}

var entry = _context.Add(new Student());


entry.CurrentValues.SetValues(StudentVM);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}

El método SetValues establece los valores de este objeto mediante la lectura de otro objeto PropertyValues.
SetValues usa la coincidencia de nombres de propiedad. No es necesario que el tipo de modelo de vista esté
relacionado con el tipo de modelo, basta con que tenga propiedades que coincidan.
El uso de StudentVM requiere que se actualice CreateVM.cshtml para usar StudentVM en lugar de Student .
En las páginas de Razor, la clase derivada PageModel es el modelo de vista.

Actualizar la página Edit


Actualice el modelo de página para la página Edit. Los cambios más importantes aparecen resaltados:
public class EditModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;

public EditModel(ContosoUniversity.Data.SchoolContext context)


{
_context = context;
}

[BindProperty]
public Student Student { get; set; }

public async Task<IActionResult> OnGetAsync(int? id)


{
if (id == null)
{
return NotFound();
}

Student = await _context.Students.FindAsync(id);

if (Student == null)
{
return NotFound();
}
return Page();
}

public async Task<IActionResult> OnPostAsync(int? id)


{
if (!ModelState.IsValid)
{
return Page();
}

var studentToUpdate = await _context.Students.FindAsync(id);

if (await TryUpdateModelAsync<Student>(
studentToUpdate,
"student",
s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
{
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}

return Page();
}
}

Los cambios de código son similares a la página Create con algunas excepciones:
OnPostAsync tiene un parámetro id opcional.
El estudiante actual se obtiene de la base de datos, en lugar de crear un estudiante vacío.
FirstOrDefaultAsync se ha reemplazado con FindAsync. FindAsync es una buena elección cuando se
selecciona una entidad de la clave principal. Vea FindAsync para obtener más información.
Probar las páginas Edit y Create
Cree y modifique algunas entidades Student.

Estados de entidad
El contexto de base de datos realiza el seguimiento de si las entidades en memoria están sincronizadas con sus
filas correspondientes en la base de datos. La información de sincronización del contexto de base de datos
determina qué ocurre cuando se llama a SaveChanges . Por ejemplo, cuando se pasa una nueva entidad al método
Add , el estado de esa entidad se establece en Added . Cuando se llama a SaveChanges , el contexto de base de
datos emite un comando INSERT de SQL.
Una entidad puede estar en uno de los estados siguientes:
Added: la entidad no existe todavía en la base de datos. El método SaveChanges emite una instrucción
INSERT.
Unchanged : no es necesario guardar cambios con esta entidad. Una entidad tiene este estado cuando se lee
desde la base de datos.
Modified : se han modificado algunos o todos los valores de propiedad de la entidad. El método
SaveChanges emite una instrucción UPDATE.

Deleted : la entidad se ha marcado para su eliminación. El método SaveChanges emite una instrucción
DELETE.
Detached : el contexto de base de datos no está realizando el seguimiento de la entidad.

En una aplicación de escritorio, los cambios de estado normalmente se establecen de forma automática. Se lee
una entidad, se realizan cambios y el estado de la entidad se cambia automáticamente a Modified . La llamada a
SaveChanges genera una instrucción UPDATE de SQL que solo actualiza las propiedades modificadas.

En una aplicación web, el DbContext que lee una entidad y muestra los datos se elimina después de representar
una página. Cuando se llama al método OnPostAsync de una página, se realiza una nueva solicitud web con una
instancia nueva de DbContext . Volver a leer la entidad en ese contexto nuevo simula el procesamiento de
escritorio.

Actualizar la página Delete


En esta sección, se agrega código para implementar un mensaje de error personalizado cuando se produce un
error en la llamada a SaveChanges . Agregue una cadena para contener los posibles mensajes de error:

public class DeleteModel : PageModel


{
private readonly ContosoUniversity.Data.SchoolContext _context;

public DeleteModel(ContosoUniversity.Data.SchoolContext context)


{
_context = context;
}

[BindProperty]
public Student Student { get; set; }
public string ErrorMessage { get; set; }

Reemplace el método OnGetAsync con el código siguiente:


public async Task<IActionResult> OnGetAsync(int? id, bool? saveChangesError = false)
{
if (id == null)
{
return NotFound();
}

Student = await _context.Students


.AsNoTracking()
.FirstOrDefaultAsync(m => m.ID == id);

if (Student == null)
{
return NotFound();
}

if (saveChangesError.GetValueOrDefault())
{
ErrorMessage = "Delete failed. Try again";
}

return Page();
}

El código anterior contiene el parámetro opcional saveChangesError . saveChangesError indica si se llamó al


método después de un error al eliminar el objeto Student. Es posible que se produzca un error en la operación de
eliminación debido a problemas de red transitorios. Los errores de red transitorios son más probables en la nube.
saveChangesError es false cuando se llama a OnGetAsync de la página Delete desde la interfaz de usuario. Cuando
OnPostAsync llama a OnGetAsync (debido a un error en la operación de eliminación), el parámetro
saveChangesError es true.

El método OnPostAsync de las páginas Delete


Reemplace OnPostAsync por el código siguiente:
public async Task<IActionResult> OnPostAsync(int? id)
{
if (id == null)
{
return NotFound();
}

var student = await _context.Students


.AsNoTracking()
.FirstOrDefaultAsync(m => m.ID == id);

if (student == null)
{
return NotFound();
}

try
{
_context.Students.Remove(student);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
catch (DbUpdateException /* ex */)
{
//Log the error (uncomment ex variable name and write a log.)
return RedirectToAction("./Delete",
new { id = id, saveChangesError = true });
}
}

En el código anterior se recupera la entidad seleccionada y después se llama al método Remove para establecer el
estado de la entidad en Deleted . Cuando se llama a SaveChanges , se genera un comando DELETE de SQL. Si se
produce un error en Remove :
Se detecta la excepción de base de datos.
Se llama al método OnGetAsync de las páginas Delete con saveChangesError=true .
Actualizar la página de Razor Delete
Agregue el siguiente mensaje de error resaltado a la página de Razor Delete.

@page "{id:int}"
@model ContosoUniversity.Pages.Students.DeleteModel

@{
ViewData["Title"] = "Delete";
}

<h2>Delete</h2>

<p class="text-danger">@Model.ErrorMessage</p>

<h3>Are you sure you want to delete this?</h3>


<div>

Pruebe Delete.

Errores comunes
Student/Home u otros vínculos no funcionan:
Compruebe que la página de Razor contiene la directiva @page correcta. Por ejemplo, la página de Razor
Student/Home no debe contener una plantilla de ruta:

@page "{id:int}"

Cada página de Razor debe incluir la directiva @page .

A N T E R IO R S IG U IE N T E
Páginas de Razor con EF Core en ASP.NET Core:
Ordenación, filtrado y paginación (3 de 8)
25/05/2018 • 26 minutes to read • Edit Online

Por Tom Dykstra, Rick Anderson y Jon P Smith


La aplicación web Contoso University muestra cómo crear aplicaciones web de las páginas de Razor con EF Core
y Visual Studio. Para obtener información sobre la serie de tutoriales, consulte el primer tutorial.
En este tutorial se agregan las funcionalidades de ordenación, filtrado, agrupación y paginación.
En la siguiente ilustración se muestra una página completa. Los encabezados de columna son vínculos
interactivos para ordenar la columna. Si se hace clic de forma consecutiva en el encabezado de una columna, el
criterio de ordenación cambia entre ascendente y descendente.

Si experimenta problemas que no puede resolver, descargue la aplicación completada para esta fase.

Agregar ordenación a la página de índice


Agregue cadenas a Students/Index.cshtml.cs PageModel para que contenga los parámetros de ordenación:
public class IndexModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;

public IndexModel(ContosoUniversity.Data.SchoolContext context)


{
_context = context;
}

public string NameSort { get; set; }


public string DateSort { get; set; }
public string CurrentFilter { get; set; }
public string CurrentSort { get; set; }

Actualice Students/Index.cshtml.cs OnGetAsync con el código siguiente:

public async Task OnGetAsync(string sortOrder)


{
NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
DateSort = sortOrder == "Date" ? "date_desc" : "Date";

IQueryable<Student> studentIQ = from s in _context.Students


select s;

switch (sortOrder)
{
case "name_desc":
studentIQ = studentIQ.OrderByDescending(s => s.LastName);
break;
case "Date":
studentIQ = studentIQ.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
studentIQ = studentIQ.OrderByDescending(s => s.EnrollmentDate);
break;
default:
studentIQ = studentIQ.OrderBy(s => s.LastName);
break;
}

Student = await studentIQ.AsNoTracking().ToListAsync();


}

El código anterior recibe un parámetro sortOrder de la cadena de consulta en la dirección URL. La aplicación
auxiliar de etiquetas delimitadoras genera la dirección URL (incluida la cadena de consulta).
El parámetro sortOrder es "Name" o "Date". Opcionalmente, el parámetro sortOrder puede ir seguido de
"_desc" para especificar el orden descendente. El criterio de ordenación predeterminado es el ascendente.
Cuando se solicita la página de índice del vínculo Students no hay ninguna cadena de consulta. Los alumnos se
muestran en orden ascendente por apellido. El orden ascendente por apellido es el valor predeterminado (caso de
paso explícito) en la instrucción switch . Cuando el usuario hace clic en un vínculo de encabezado de columna, se
proporciona el valor sortOrder correspondiente en el valor de la cadena de consulta.
La página de Razor usa NameSort y DateSort para configurar los hipervínculos del encabezado de columna con
los valores de cadena de consulta adecuados:
public async Task OnGetAsync(string sortOrder)
{
NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
DateSort = sortOrder == "Date" ? "date_desc" : "Date";

IQueryable<Student> studentIQ = from s in _context.Students


select s;

switch (sortOrder)
{
case "name_desc":
studentIQ = studentIQ.OrderByDescending(s => s.LastName);
break;
case "Date":
studentIQ = studentIQ.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
studentIQ = studentIQ.OrderByDescending(s => s.EnrollmentDate);
break;
default:
studentIQ = studentIQ.OrderBy(s => s.LastName);
break;
}

Student = await studentIQ.AsNoTracking().ToListAsync();


}

El código siguiente contiene el operador ?: de C#:

NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";


DateSort = sortOrder == "Date" ? "date_desc" : "Date";

La primera línea especifica que, cuando sortOrder es NULL o está vacío, NameSort se establece en "name_desc".
Si sortOrder no es NULL ni está vacío, NameSort se establece en una cadena vacía.
El ?: operator también se conoce como el operador ternario.
Estas dos instrucciones habilitan la página para establecer los hipervínculos de encabezado de columna de la
siguiente forma:

CRITERIO DE ORDENACIÓN ACTUAL HIPERVÍNCULO DE APELLIDO HIPERVÍNCULO DE FECHA

Apellido: ascendente descending ascending

Apellido: descendente ascending ascending

Fecha: ascendente ascending descending

Fecha: descendente ascending ascending

El método usa LINQ to Entities para especificar la columna por la que se va a ordenar. El código inicializa un
IQueryable<Student> antes de la instrucción switch y lo modifica en la instrucción switch:
public async Task OnGetAsync(string sortOrder)
{
NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
DateSort = sortOrder == "Date" ? "date_desc" : "Date";

IQueryable<Student> studentIQ = from s in _context.Students


select s;

switch (sortOrder)
{
case "name_desc":
studentIQ = studentIQ.OrderByDescending(s => s.LastName);
break;
case "Date":
studentIQ = studentIQ.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
studentIQ = studentIQ.OrderByDescending(s => s.EnrollmentDate);
break;
default:
studentIQ = studentIQ.OrderBy(s => s.LastName);
break;
}

Student = await studentIQ.AsNoTracking().ToListAsync();


}

Cuando se crea o se modifica un IQueryable , no se envía ninguna consulta a la base de datos. La consulta no se
ejecuta hasta que el objeto IQueryable se convierte en una colección. IQueryable se convierte en una colección
mediante una llamada a un método como ToListAsync . Por lo tanto, el código IQueryable produce una única
consulta que no se ejecuta hasta la siguiente instrucción:

Student = await studentIQ.AsNoTracking().ToListAsync();

OnGetAsync podría detallarse con un gran número de columnas.


Agregar hipervínculos de encabezado de columna a la página de índice de Student
Reemplace el código de Students/Index.cshtml con el siguiente código resaltado:
@page
@model ContosoUniversity.Pages.Students.IndexModel

@{
ViewData["Title"] = "Index";
}

<h2>Index</h2>
<p>
<a asp-page="Create">Create New</a>
</p>

<table class="table">
<thead>
<tr>
<th>
<a asp-page="./Index" asp-route-sortOrder="@Model.NameSort">
@Html.DisplayNameFor(model => model.Student[0].LastName)
</a>
</th>
<th>
@Html.DisplayNameFor(model => model.Student[0].FirstMidName)
</th>
<th>
<a asp-page="./Index" asp-route-sortOrder="@Model.DateSort">
@Html.DisplayNameFor(model => model.Student[0].EnrollmentDate)
</a>
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Student)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.LastName)
</td>
<td>
@Html.DisplayFor(modelItem => item.FirstMidName)
</td>
<td>
@Html.DisplayFor(modelItem => item.EnrollmentDate)
</td>
<td>
<a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>

El código anterior:
Agrega hipervínculos a los encabezados de columna LastName y EnrollmentDate .
Usa la información de NameSort y DateSort para configurar hipervínculos con los valores de criterio de
ordenación actuales.
Para comprobar que la ordenación funciona:
Ejecute la aplicación y haga clic en la pestaña Students.
Haga clic en Last Name.
Haga clic en Enrollment Date.
Para comprender mejor el código:
En Student/Index.cshtml.cs, establezca un punto de interrupción en switch (sortOrder) .
Agregue una inspección para NameSort y DateSort .
En Student/Index.cshtml, establezca un punto de interrupción en
@Html.DisplayNameFor(model => model.Student[0].LastName) .

Ejecute paso a paso el depurador.

Agregar un cuadro de búsqueda a la página de índice de Students


Para agregar un filtro a la página de índice de Students:
Se agrega un cuadro de texto y un botón de envío a la página de Razor. El cuadro de texto proporciona una
cadena de búsqueda de nombre o apellido.
El modelo de página se actualiza para usar el valor del cuadro de texto.
Agregar la funcionalidad de filtrado al método Index
Actualice Students/Index.cshtml.cs OnGetAsync con el código siguiente:

public async Task OnGetAsync(string sortOrder, string searchString)


{
NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
DateSort = sortOrder == "Date" ? "date_desc" : "Date";
CurrentFilter = searchString;

IQueryable<Student> studentIQ = from s in _context.Students


select s;
if (!String.IsNullOrEmpty(searchString))
{
studentIQ = studentIQ.Where(s => s.LastName.Contains(searchString)
|| s.FirstMidName.Contains(searchString));
}

switch (sortOrder)
{
case "name_desc":
studentIQ = studentIQ.OrderByDescending(s => s.LastName);
break;
case "Date":
studentIQ = studentIQ.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
studentIQ = studentIQ.OrderByDescending(s => s.EnrollmentDate);
break;
default:
studentIQ = studentIQ.OrderBy(s => s.LastName);
break;
}

Student = await studentIQ.AsNoTracking().ToListAsync();


}

El código anterior:
Agrega el parámetro searchString al método OnGetAsync . El valor de la cadena de búsqueda se recibe desde
un cuadro de texto que se agrega en la siguiente sección.
Se agregó una cláusula Where a la instrucción LINQ. La cláusula Where selecciona solo los alumnos cuyo
nombre o apellido contienen la cadena de búsqueda. La instrucción LINQ se ejecuta solo si hay un valor para
buscar.
Nota: El código anterior llama al método Where en un objeto IQueryable y el filtro se procesa en el servidor. En
algunos escenarios, la aplicación puede hacer una llamada al método Where como un método de extensión en
una colección en memoria. Por ejemplo, suponga que _context.Students cambia de DbSet de EF Core a un
método de repositorio que devuelve una colección IEnumerable . Lo más habitual es que el resultado fuera el
mismo, pero en algunos casos puede ser diferente.
Por ejemplo, la implementación de .NET Framework de Contains realiza una comparación que distingue
mayúsculas de minúsculas de forma predeterminada. En SQL Server, la distinción entre mayúsculas y minúsculas
de Contains viene determinada por la configuración de intercalación de la instancia de SQL Server. SQL Server
no diferencia entre mayúsculas y minúsculas de forma predeterminada. Se podría llamar a ToUpper para hacer
explícitamente que la prueba no distinga entre mayúsculas y minúsculas:
Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper())

El código anterior garantiza que los resultados no distingan entre mayúsculas y minúsculas si cambia el código
para que use IEnumerable . Cuando se llama a Contains en una colección IEnumerable , se usa la implementación
de .NET Core. Cuando se llama a Contains en un objeto IQueryable , se usa la implementación de la base de
datos. Devolver un IEnumerable desde un repositorio puede acarrear una disminución significativa del
rendimiento:
1. Todas las filas se devuelven desde el servidor de base de datos.
2. El filtro se aplica a todas las filas devueltas en la aplicación.
Hay una disminución del rendimiento por llamar a ToUpper . El código ToUpper agrega una función en la cláusula
WHERE de la instrucción SELECT de TSQL. La función agregada impide que el optimizador use un índice. Dado
que SQL está instalado para no distinguir entre mayúsculas y minúsculas, es mejor evitar llamar a ToUpper
cuando no sea necesario.
Agregar un cuadro de búsqueda a la página de índice de Student
En Pages/Student/Index.cshtml, agregue el siguiente código resaltado para crear un botón Search y cromo
ordenado.

@page
@model ContosoUniversity.Pages.Students.IndexModel

@{
ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
<a asp-page="Create">Create New</a>
</p>

<form asp-page="./Index" method="get">


<div class="form-actions no-color">
<p>
Find by name:
<input type="text" name="SearchString" value="@Model.CurrentFilter" />
<input type="submit" value="Search" class="btn btn-default" /> |
<a asp-page="./Index">Back to full List</a>
</p>
</div>
</form>

<table class="table">
El código anterior usa la aplicación auxiliar de etiquetas <form> para agregar el cuadro de texto de búsqueda y el
botón. De forma predeterminada, la aplicación auxiliar de etiquetas <form> envía datos de formulario con POST.
Con POST, los parámetros se pasan en el cuerpo del mensaje HTTP y no en la dirección URL. Cuando se usa el
método HTTP GET, los datos del formulario se pasan en la dirección URL como cadenas de consulta. Pasar los
datos con cadenas de consulta permite a los usuarios marcar la dirección URL. Las directrices de W3C
recomiendan el uso de GET cuando la acción no produzca ninguna actualización.
Pruebe la aplicación:
Seleccione la pestaña Students y escriba una cadena de búsqueda.
Seleccione Search.
Fíjese en que la dirección URL contiene la cadena de búsqueda.

http://localhost:5000/Students?SearchString=an

Si se colocó un marcador en la página, el marcador contiene la dirección URL a la página y la cadena de consulta
de SearchString . El method="get" en la etiqueta form es lo que ha provocado que se generara la cadena de
consulta.
Actualmente, cuando se selecciona un vínculo de ordenación del encabezado de columna, el filtro de valor del
cuadro Search se pierde. El valor de filtro perdido se fija en la sección siguiente.

Agregar la funcionalidad de paginación a la página de índice de


Students
En esta sección, se crea una clase PaginatedList para admitir la paginación. La clase PaginatedList usa las
instrucciones Skip y Take para filtrar los datos en el servidor en lugar de recuperar todas las filas de la tabla. La
ilustración siguiente muestra los botones de paginación.

En la carpeta del proyecto, cree PaginatedList.cs con el código siguiente:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity
{
public class PaginatedList<T> : List<T>
{
public int PageIndex { get; private set; }
public int TotalPages { get; private set; }

public PaginatedList(List<T> items, int count, int pageIndex, int pageSize)


{
PageIndex = pageIndex;
TotalPages = (int)Math.Ceiling(count / (double)pageSize);

this.AddRange(items);
}

public bool HasPreviousPage


{
get
{
return (PageIndex > 1);
}
}

public bool HasNextPage


{
get
{
return (PageIndex < TotalPages);
}
}

public static async Task<PaginatedList<T>> CreateAsync(


IQueryable<T> source, int pageIndex, int pageSize)
{
var count = await source.CountAsync();
var items = await source.Skip(
(pageIndex - 1) * pageSize)
.Take(pageSize).ToListAsync();
return new PaginatedList<T>(items, count, pageIndex, pageSize);
}
}
}

El método CreateAsync en el código anterior toma el tamaño y el número de la página, y aplica las instrucciones
Skip y Take correspondientes a IQueryable . Cuando ToListAsync se llama en IQueryable , devuelve una lista
que solo contiene la página solicitada. Las propiedades HasPreviousPage y HasNextPage se usan para habilitar o
deshabilitar los botones de página Previous y Next.
El método CreateAsync se usa para crear la PaginatedList<T> . No se puede crear un constructor del objeto
PaginatedList<T> , los constructores no pueden ejecutar código asincrónico.

Agregar la funcionalidad de paginación al método Index


En Students/Index.cshtml.cs, actualice el tipo de Student de IList<Student> a PaginatedList<Student> :

public PaginatedList<Student> Student { get; set; }


Actualice Students/Index.cshtml.cs OnGetAsync con el código siguiente:

public async Task OnGetAsync(string sortOrder,


string currentFilter, string searchString, int? pageIndex)
{
CurrentSort = sortOrder;
NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
DateSort = sortOrder == "Date" ? "date_desc" : "Date";
if (searchString != null)
{
pageIndex = 1;
}
else
{
searchString = currentFilter;
}

CurrentFilter = searchString;

IQueryable<Student> studentIQ = from s in _context.Students


select s;
if (!String.IsNullOrEmpty(searchString))
{
studentIQ = studentIQ.Where(s => s.LastName.Contains(searchString)
|| s.FirstMidName.Contains(searchString));
}
switch (sortOrder)
{
case "name_desc":
studentIQ = studentIQ.OrderByDescending(s => s.LastName);
break;
case "Date":
studentIQ = studentIQ.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
studentIQ = studentIQ.OrderByDescending(s => s.EnrollmentDate);
break;
default:
studentIQ = studentIQ.OrderBy(s => s.LastName);
break;
}

int pageSize = 3;
Student = await PaginatedList<Student>.CreateAsync(
studentIQ.AsNoTracking(), pageIndex ?? 1, pageSize);
}

El código anterior agrega el índice de la página, el sortOrder actual y el currentFilter a la firma del método.

public async Task OnGetAsync(string sortOrder,


string currentFilter, string searchString, int? pageIndex)

Todos los parámetros son NULL cuando:


Se llama a la página desde el vínculo Students.
El usuario no ha seleccionado un vínculo de ordenación o paginación.
Cuando se hace clic en un vínculo de paginación, la variable de índice de página contiene el número de página
que se tiene que mostrar.
CurrentSort proporciona la página de Razor con el criterio de ordenación actual. Se debe incluir el criterio de
ordenación actual en los vínculos de paginación para mantener el criterio de ordenación durante la paginación.
CurrentFilter proporciona la página de Razor con la cadena del filtro actual. El valor CurrentFilter :
Debe incluirse en los vínculos de paginación para mantener la configuración del filtro durante la paginación.
Debe restaurarse en el cuadro de texto cuando se vuelva a mostrar la página.
Si se cambia la cadena de búsqueda durante la paginación, la página se restablece a 1. La página debe
restablecerse a 1 porque el nuevo filtro puede hacer que se muestren diferentes datos. Cuando se escribe un
valor de búsqueda y se selecciona Submit:
La cadena de búsqueda cambia.
El parámetro searchString no es NULL.

if (searchString != null)
{
pageIndex = 1;
}
else
{
searchString = currentFilter;
}

El método PaginatedList.CreateAsync convierte la consulta del alumno en una sola página de alumnos de un tipo
de colección que admita la paginación. Esa única página de alumnos se pasa a la página de Razor.

Student = await PaginatedList<Student>.CreateAsync(


studentIQ.AsNoTracking(), pageIndex ?? 1, pageSize);

Los dos signos de interrogación en PaginatedList.CreateAsync representan el [operador de uso combinado de


NULL ](https://docs.microsoft.com/ dotnet/csharp/language-reference/operators/null-conditional-operator). El
operador de uso combinado de NULL define un valor predeterminado para un tipo que acepta valores NULL. La
expresión (pageIndex ?? 1) significa devolver el valor de pageIndex si tiene un valor. Devuelve 1 si pageIndex
no tiene ningún valor.

Agregar vínculos de paginación a la página de Razor de alumno


Actualice el marcado en Students/Index.cshtml. Se resaltan los cambios:

@page
@model ContosoUniversity.Pages.Students.IndexModel

@{
ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
<a asp-page="Create">Create New</a>
</p>

<form asp-page="./Index" method="get">


<div class="form-actions no-color">
<p>
Find by name: <input type="text" name="SearchString" value="@Model.CurrentFilter" />
<input type="submit" value="Search" class="btn btn-default" /> |
<a asp-page="./Index">Back to full List</a>
</p>
</div>
</form>
<table class="table">
<thead>
<tr>
<th>
<a asp-page="./Index" asp-route-sortOrder="@Model.NameSort"
asp-route-currentFilter="@Model.CurrentFilter">
@Html.DisplayNameFor(model => model.Student[0].LastName)
</a>
</th>
<th>
@Html.DisplayNameFor(model => model.Student[0].FirstMidName)
</th>
<th>
<a asp-page="./Index" asp-route-sortOrder="@Model.DateSort"
asp-route-currentFilter="@Model.CurrentFilter">
@Html.DisplayNameFor(model => model.Student[0].EnrollmentDate)
</a>
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Student) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.LastName)
</td>
<td>
@Html.DisplayFor(modelItem => item.FirstMidName)
</td>
<td>
@Html.DisplayFor(modelItem => item.EnrollmentDate)
</td>
<td>
<a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>

@{
var prevDisabled = !Model.Student.HasPreviousPage ? "disabled" : "";
var nextDisabled = !Model.Student.HasNextPage ? "disabled" : "";
}

<a asp-page="./Index"
asp-route-sortOrder="@Model.CurrentSort"
asp-route-pageIndex="@(Model.Student.PageIndex - 1)"
asp-route-currentFilter="@Model.CurrentFilter"
class="btn btn-default @prevDisabled">
Previous
</a>
<a asp-page="./Index"
asp-route-sortOrder="@Model.CurrentSort"
asp-route-pageIndex="@(Model.Student.PageIndex + 1)"
asp-route-currentFilter="@Model.CurrentFilter"
class="btn btn-default @nextDisabled">
Next
</a>

Los vínculos del encabezado de la columna usan la cadena de consulta para pasar la cadena de búsqueda actual al
método OnGetAsync , de modo que el usuario pueda ordenar los resultados del filtro:
<a asp-page="./Index" asp-route-sortOrder="@Model.NameSort"
asp-route-currentFilter="@Model.CurrentFilter">
@Html.DisplayNameFor(model => model.Student[0].LastName)
</a>

Los botones de paginación se muestran mediante aplicaciones auxiliares de etiquetas:

<a asp-page="./Index"
asp-route-sortOrder="@Model.CurrentSort"
asp-route-pageIndex="@(Model.Student.PageIndex - 1)"
asp-route-currentFilter="@Model.CurrentFilter"
class="btn btn-default @prevDisabled">
Previous
</a>
<a asp-page="./Index"
asp-route-sortOrder="@Model.CurrentSort"
asp-route-pageIndex="@(Model.Student.PageIndex + 1)"
asp-route-currentFilter="@Model.CurrentFilter"
class="btn btn-default @nextDisabled">
Next
</a>

Ejecute la aplicación y vaya a la página Students.


Para comprobar que la paginación funciona correctamente, haga clic en los vínculos de paginación en distintos
criterios de ordenación.
Para comprobar que la paginación también funciona correctamente con filtrado y ordenación, escriba una
cadena de búsqueda e intente llevar a cabo la paginación de nuevo.

Para comprender mejor el código:


En Student/Index.cshtml.cs, establezca un punto de interrupción en switch (sortOrder) .
Agregue una inspección para NameSort , DateSort , CurrentSort y Model.Student.PageIndex .
En Student/Index.cshtml, establezca un punto de interrupción en
@Html.DisplayNameFor(model => model.Student[0].LastName) .
Ejecute paso a paso el depurador.

Actualizar la página About para mostrar las estadísticas de los alumnos


En este paso, se actualiza Pages/About.cshtml para mostrar cuántos alumnos se han inscrito por cada fecha de
inscripción. La actualización usa la agrupación e incluye los siguientes pasos:
Cree una clase de modelo de vista para los datos usados por la página About.
Modifique el modelo de página y la página de Razor About.
Creación del modelo de vista
Cree una carpeta SchoolViewModels en la carpeta Models.
En la carpeta SchoolViewModels, agregue EnrollmentDateGroup.cs con el código siguiente:

using System;
using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models.SchoolViewModels
{
public class EnrollmentDateGroup
{
[DataType(DataType.Date)]
public DateTime? EnrollmentDate { get; set; }

public int StudentCount { get; set; }


}
}

Actualizar el modelo de la página About


Actualice el archivo Pages/About.cshtml.cs con el código siguiente:
using ContosoUniversity.Data;
using ContosoUniversity.Models.SchoolViewModels;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages
{
public class AboutModel : PageModel
{
private readonly SchoolContext _context;

public AboutModel(SchoolContext context)


{
_context = context;
}

public IList<EnrollmentDateGroup> Student { get; set; }

public async Task OnGetAsync()


{
IQueryable<EnrollmentDateGroup> data =
from student in _context.Students
group student by student.EnrollmentDate into dateGroup
select new EnrollmentDateGroup()
{
EnrollmentDate = dateGroup.Key,
StudentCount = dateGroup.Count()
};

Student = await data.AsNoTracking().ToListAsync();


}
}
}

La instrucción LINQ agrupa las entidades de alumnos por fecha de inscripción, calcula la cantidad de entidades
que se incluyen en cada grupo y almacena los resultados en una colección de objetos de modelo de la vista
EnrollmentDateGroup .

Nota: EF Core actualmente no admite el comando group de LINQ. En el código anterior, se devuelven todos los
registros de alumnos desde SQL Server. El comando group se aplica en la aplicación de las páginas de Razor, no
en SQL Server. EF Core 2.1 admitirá este operador group de LINQ y la agrupación se produce en SQL Server.
Vea Relational: Support translating GroupBy() to SQL (Relacional: compatibilidad para traducir GroupBy() para
SQL ). EF Core 2.1 se publicará con .NET Core 2.1. Para obtener más información, vea la página Mapa de ruta de
.NET Core.
Modificar la página de Razor About
Reemplace el código del archivo Pages/About.cshtml por el código siguiente:
@page
@model ContosoUniversity.Pages.AboutModel

@{
ViewData["Title"] = "Student Body Statistics";
}

<h2>Student Body Statistics</h2>

<table>
<tr>
<th>
Enrollment Date
</th>
<th>
Students
</th>
</tr>

@foreach (var item in Model.Student)


{
<tr>
<td>
@Html.DisplayFor(modelItem => item.EnrollmentDate)
</td>
<td>
@item.StudentCount
</td>
</tr>
}
</table>

Ejecute la aplicación y vaya a la página About. En una tabla se muestra el número de alumnos para cada fecha de
inscripción.
Si experimenta problemas que no puede resolver, descargue la aplicación completada para esta fase.

Recursos adicionales
Depuración del código fuente de ASP.NET Core 2.x
En el tutorial siguiente, la aplicación usa las migraciones para actualizar el modelo de datos.

A N T E R IO R S IG U IE N T E
Páginas de Razor con EF Core en ASP.NET Core:
Migraciones (4 de 8)
25/06/2018 • 17 minutes to read • Edit Online

Por Tom Dykstra, Jon P Smith y Rick Anderson


La aplicación web Contoso University muestra cómo crear aplicaciones web de las páginas de Razor con EF Core
y Visual Studio. Para obtener información sobre la serie de tutoriales, consulte el primer tutorial.
En este tutorial, se usa la característica de migraciones de EF Core para administrar cambios en el modelo de
datos.
Si experimenta problemas que no puede resolver, descargue la aplicación completada para esta fase.
Cuando se desarrolla una aplicación nueva, el modelo de datos cambia con frecuencia. Cada vez que el modelo
cambia, este deja de estar sincronizado con la base de datos. Este tutorial se inició con la configuración de Entity
Framework para crear la base de datos si no existía. Cada vez que los datos del modelo cambian:
Se quita la base de datos.
EF crea una que coincide con el modelo.
La aplicación inicializa la base de datos con datos de prueba.
Este enfoque para mantener la base de datos sincronizada con el modelo de datos funciona bien hasta que la
aplicación se implemente en producción. Cuando se ejecuta la aplicación en producción, normalmente está
almacenando datos que hay que mantener. No se puede iniciar la aplicación con una prueba de base de datos
cada vez que se hace un cambio (por ejemplo, agregar una nueva columna). La característica Migraciones de EF
Core soluciona este problema habilitando EF Core para actualizar el esquema de la base de datos en lugar de
crear una.
En lugar de quitar y volver a crear la base de datos cuando los datos del modelo cambian, las migraciones
actualizan el esquema y conservan los datos existentes.

Paquetes de Entity Framework Core NuGet para migraciones


Para trabajar con las migraciones, use la Consola del Administrador de paquetes (PMC ) o la interfaz de la
línea de comandos (CLI). En estos tutoriales se muestra cómo usar los comandos de la CLI. Al final de este
tutorial encontrará información sobre la PMC.
Las herramientas de EF Core para la interfaz de la línea de comandos (CLI) se proporcionan en
Microsoft.EntityFrameworkCore.Tools.DotNet. Para instalar este paquete, agréguelo a la colección
DotNetCliToolReference del archivo .csproj, como se muestra a continuación. Nota: Debe instalarse este paquete
mediante la edición del archivo .csproj. No se puede usar el comando install-package o la interfaz gráfica de
usuario del administrador de paquetes para instalar este paquete. Edite el archivo .csproj; para ello, haga clic con
el botón derecho en el nombre del proyecto en el Explorador de soluciones y seleccione Editar
ContosoUniversity.csproj.
El siguiente marcado muestra el archivo .csproj actualizado con las herramientas de la CLI de EF Core resaltadas:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Utils" Version="2.0.0" />
</ItemGroup>
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.0" />
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.0" />
</ItemGroup>
</Project>

Los números de versión en el ejemplo anterior eran los actuales cuando se escribió el tutorial. Use la misma
versión para las herramientas de la CLI de EF Core disponibles en los otros paquetes.

Cambiar la cadena de conexión


En el archivo appsettings.json, cambie el nombre de la base de datos en la cadena de conexión a
ContosoUniversity2.

{
"ConnectionStrings": {
"DefaultConnection": "Server=
(localdb)\\mssqllocaldb;Database=ContosoUniversity2;ConnectRetryCount=0;Trusted_Connection=True;MultipleActiv
eResultSets=true"
},

Cambiar el nombre de la base de datos en la cadena de conexión hace que la primera migración cree una base de
datos. Se crea una base de datos porque no existe ninguna con ese nombre. No es necesario cambiar la cadena
de conexión para comenzar a usar las migraciones.
Una alternativa a cambiar el nombre de la base de datos es eliminarla. Use el Explorador de objetos de SQL
Server (SSOX) o el comando de la CLI database drop :

dotnet ef database drop

En la siguiente sección se explica cómo ejecutar comandos de la CLI.

Crear una migración inicial


Compile el proyecto.
Abra una ventana de comandos y desplácese hasta la carpeta del proyecto. La carpeta del proyecto contiene el
archivo Startup.cs.
Escriba lo siguiente en la ventana de comandos:

dotnet ef migrations add InitialCreate

La ventana de comandos muestra información similar a la siguiente:


info: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[0]
User profile is available. Using 'C:\Users\username\AppData\Local\ASP.NET\DataProtection-Keys' as key
repository and Windows DPAPI to encrypt keys at rest.
info: Microsoft.EntityFrameworkCore.Infrastructure[100403]
Entity Framework Core 2.0.0-rtm-26452 initialized 'SchoolContext' using provider
'Microsoft.EntityFrameworkCore.SqlServer' with options: None
Done. To undo this action, use 'ef migrations remove'

Si se produce un error en la migración y se muestra el mensaje "No se puede tener acceso al archivo...
ContosoUniversity.dll ya que otro proceso lo está usando." :
Detenga IIS Express.
Salga y reinicie Visual Studio o
Busque el icono de IIS Express en la bandeja del sistema de Windows.
Haga clic con el botón derecho en el icono de IIS Express y, después, haga clic en ContosoUniversity
> Detener sitio.
Si se muestra el mensaje de error "Error de complicación.", vuelva a ejecutar el comando. Si obtiene este error,
deje una nota en la parte inferior de este tutorial.
Examinar los métodos Up y Down
El comando de EF Core migrations add generó código desde donde crear la base de datos. Este código de
migraciones se encuentra en el archivo Migrations<marca_de_tiempo>_InitialCreate.cs. El método Up de la
clase InitialCreate crea las tablas de base de datos que corresponden a los conjuntos de entidades del modelo
de datos. El método Down las elimina, tal como se muestra en el ejemplo siguiente:

public partial class InitialCreate : Migration


{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Course",
columns: table => new
{
CourseID = table.Column<int>(type: "int", nullable: false),
Credits = table.Column<int>(type: "int", nullable: false),
Title = table.Column<string>(type: "nvarchar(max)", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Course", x => x.CourseID);
});

protected override void Down(MigrationBuilder migrationBuilder)


{
migrationBuilder.DropTable(
name: "Enrollment");

migrationBuilder.DropTable(
name: "Course");

migrationBuilder.DropTable(
name: "Student");
}
}
}

Las migraciones llaman al método Up para implementar los cambios del modelo de datos para una migración.
Cuando se escribe un comando para revertir la actualización, las migraciones llaman al método Down .
El código anterior es para la migración inicial. Ese código se creó cuando se ejecutó el comando
migrations add InitialCreate . El parámetro de nombre de la migración ("InitialCreate" en el ejemplo) se usa para
el nombre de archivo. El nombre de la migración puede ser cualquier nombre de archivo válido. Es más
recomendable elegir una palabra o frase que resuma lo que se hace en la migración. Por ejemplo, una migración
que ha agregado una tabla de departamento podría denominarse "AddDepartmentTable".
Si la migración inicial está creada y la base de datos existe:
Se genera el código de creación de la base de datos.
El código de creación de la base de datos no tiene que ejecutarse porque la base de datos ya coincide con el
modelo de datos. Si el código de creación de la base de datos se está ejecutando, no hace ningún cambio
porque la base de datos ya coincide con el modelo de datos.
Cuando la aplicación se implementa en un entorno nuevo, se debe ejecutar el código de creación de la base de
datos para crear la base de datos.
Anteriormente, la cadena de conexión se cambió para usar un nombre nuevo para la base de datos. La base de
datos especificada no existe, por lo que las migraciones crean la base de datos.
La instantánea del modelo de datos
Las migraciones crean una instantánea del esquema de la base de datos actual en
Migrations/SchoolContextModelSnapshot.cs. Cuando se agrega una migración, EF determina qué ha cambiado
mediante la comparación del modelo de datos con el archivo de instantánea.
Cuando elimine una migración, use el comando dotnet ef migrations remove. dotnet ef migrations remove
elimina la migración y garantiza que la instantánea se restablece correctamente.
Vea Migraciones en entornos de equipo para más información sobre cómo se usa el archivo de instantánea.

Quitar EnsureCreated
Para el desarrollo inicial se usó el comando EnsureCreated . En este tutorial, se usan las migraciones.
EnsureCreated tiene las siguientes limitaciones:

Omite las migraciones y crea la base de datos y el esquema.


No crea una tabla de migraciones.
No puede usarse con las migraciones.
Está diseñado para crear prototipos rápidos o de prueba donde se quita y vuelve a crear la base de datos con
frecuencia.
Quite las siguientes líneas de DbInitializer :

context.Database.EnsureCreated();

Aplicar la migración a la base de datos en desarrollo


En la ventana de comandos, escriba lo siguiente para crear la base de datos y las tablas.

dotnet ef database update

Nota: Si el comando update devuelve el error "Error de compilación.":


Vuelva a ejecutar el comando.
Si se vuelve a producir un error, salga de Visual Studio y luego ejecute el comando update .
Deje un mensaje en la parte inferior de la página.
El resultado del comando es similar al resultado del comando migrations add . En el comando anterior se
muestran los registros para los comandos de SQL que configuran la base de datos. La mayoría de los registros se
omite en la siguiente salida de ejemplo:

info: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[0]
User profile is available. Using 'C:\Users\username\AppData\Local\ASP.NET\DataProtection-Keys' as key
repository and Windows DPAPI to encrypt keys at rest.
info: Microsoft.EntityFrameworkCore.Infrastructure[100403]
Entity Framework Core 2.0.0-rtm-26452 initialized 'SchoolContext' using provider
'Microsoft.EntityFrameworkCore.SqlServer' with options: None
info: Microsoft.EntityFrameworkCore.Database.Command[200101]
Executed DbCommand (467ms) [Parameters=[], CommandType='Text', CommandTimeout='60']
CREATE DATABASE [ContosoUniversity2];
info: Microsoft.EntityFrameworkCore.Database.Command[200101]
Executed DbCommand (20ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
CREATE TABLE [__EFMigrationsHistory] (
[MigrationId] nvarchar(150) NOT NULL,
[ProductVersion] nvarchar(32) NOT NULL,
CONSTRAINT [PK___EFMigrationsHistory] PRIMARY KEY ([MigrationId])
);

<logs omitted for brevity>

info: Microsoft.EntityFrameworkCore.Database.Command[200101]
Executed DbCommand (3ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES (N'20170816151242_InitialCreate', N'2.0.0-rtm-26452');
Done.

Para reducir el nivel de detalle en los mensajes de registro, cámbielo en el archivo appsettings.Development.json.
Para obtener más información, vea Introducción al registro.
Use el Explorador de objetos de SQL Server para inspeccionar la base de datos. Observe la adición de una
tabla __EFMigrationsHistory . La tabla __EFMigrationsHistory realiza un seguimiento de las migraciones que se
han aplicado a la base de datos. Examine los datos de la tabla __EFMigrationsHistory , muestra una fila para la
primera migración. En el último registro del ejemplo de salida de la CLI anterior se muestra la instrucción
INSERT que crea esta fila.
Ejecute la aplicación y compruebe que todo funciona correctamente.

Aplicar las migraciones en producción


Se recomienda que las aplicaciones de producción no llamen a Database.Migrate al iniciar la aplicación. No debe
llamarse a Migrate desde una aplicación en la granja de servidores. Por ejemplo, si la aplicación se ha
implementado en la nube con escalado horizontal (se ejecutan varias instancias de la aplicación).
La migración de bases de datos debe realizarse como parte de la implementación y de un modo controlado. Entre
los métodos de migración de base de datos de producción se incluyen:
Uso de las migraciones para crear scripts SQL y uso de scripts SQL en la implementación.
Ejecución de dotnet ef database update desde un entorno controlado.

EF Core usa la tabla __MigrationsHistory para ver si es necesario ejecutar las migraciones. Si la base de datos
está actualizada, no se ejecuta la migración.

Diferencias entre la interfaz de la línea de comandos (CLI) y la Consola


del Administrador de paquetes (PMC)
El conjunto de herramientas de EF Core para administrar las migraciones está disponible desde:
Comandos de la CLI de .NET Core.
Los cmdlets de PowerShell en la ventana Consola del Administrador de paquetes (PMC ) de Visual Studio.
Este tutorial muestra cómo usar la CLI, algunos desarrolladores prefieren usar la PMC.
Los comandos de EF Core para la PMC están en el paquete Microsoft.EntityFrameworkCore.Tools. Este paquete
se incluye en el metapaquete Microsoft.AspNetCore.All, por lo que no es necesario instalarlo.
Importante: Este no es el mismo paquete que el que se instala para la CLI mediante la edición del archivo .csproj.
El nombre de este paquete termina en Tools , a diferencia del nombre de paquete de la CLI que termina en
Tools.DotNet .

Para obtener más información sobre los comandos de la CLI, vea CLI de .NET Core.
Para obtener más información sobre los comandos de la PMC, vea Consola del Administrador de paquetes
(Visual Studio).

Solución de problemas
Descargue la aplicación completa de esta fase.
La aplicación genera la siguiente excepción:

SqlException: Cannot open database "ContosoUniversity" requested by the login.


The login failed.
Login failed for user 'user name'.

Solución: ejecute dotnet ef database update

Si el comando update devuelve el error "Error de compilación.":


Vuelva a ejecutar el comando.
Deje un mensaje en la parte inferior de la página.

A N T E R IO R S IG U IE N T E
Páginas de Razor con EF Core en ASP.NET Core:
Modelo de datos (5 de 8)
25/06/2018 • 48 minutes to read • Edit Online

Por Tom Dykstra y Rick Anderson


La aplicación web Contoso University muestra cómo crear aplicaciones web de las páginas de Razor con EF Core
y Visual Studio. Para obtener información sobre la serie de tutoriales, consulte el primer tutorial.
En los tutoriales anteriores se trabajaba con un modelo de datos básico que se componía de tres entidades. En
este tutorial:
Se agregan más entidades y relaciones.
Se personaliza el modelo de datos especificando el formato, la validación y las reglas de asignación de la base
de datos.
Las clases de entidad para el modelo de datos completo se muestran en la siguiente ilustración:
Si experimenta problemas que no puede resolver, descargue la aplicación completada para esta fase.

Personalizar el modelo de datos con atributos


En esta sección, se personaliza el modelo de datos mediante atributos.
El atributo DataType
Las páginas de alumno actualmente muestran la hora de la fecha de inscripción. Normalmente, los campos de
fecha muestran solo la fecha y no la hora.
Actualice Models/Student.cs con el siguiente código resaltado:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models
{
public class Student
{
public int ID { get; set; }
public string LastName { get; set; }
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime EnrollmentDate { get; set; }

public ICollection<Enrollment> Enrollments { get; set; }


}
}

El atributo DataType especifica un tipo de datos más específico que el tipo intrínseco de base de datos. En este
caso solo se debe mostrar la fecha, no la fecha y hora. La enumeración DataType proporciona muchos tipos de
datos, como Date (Fecha), Time (Hora), PhoneNumber (Número de teléfono), Currency (Divisa), EmailAddress
(Dirección de correo electrónico), etc. El atributo DataType también puede permitir que la aplicación proporcione
automáticamente características específicas del tipo. Por ejemplo:
El vínculo mailto: se crea automáticamente para DataType.EmailAddress .
El selector de fecha se proporciona para DataType.Date en la mayoría de los exploradores.
El atributo DataType emite atributos HTML 5 data- (se pronuncia "datos dash") para su uso por parte de los
exploradores HTML 5. Los atributos DataType no proporcionan validación.
DataType.Date no especifica el formato de la fecha que se muestra. De manera predeterminada, el campo de
fecha se muestra según los formatos predeterminados basados en el elemento CultureInfo del servidor.
El atributo DisplayFormat se usa para especificar el formato de fecha de forma explícita:

[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]

La configuración ApplyFormatInEditMode especifica que el formato también debe aplicarse a la interfaz de usuario
de edición. Algunos campos no deben usar ApplyFormatInEditMode . Por ejemplo, el símbolo de divisa
generalmente no debe mostrarse en un cuadro de texto de edición.
El atributo DisplayFormat puede usarse por sí solo. Normalmente se recomienda usar el atributo DataType con el
atributo DisplayFormat . El atributo DataType transmite la semántica de los datos en lugar de cómo se
representan en una pantalla. El atributo DataType proporciona las siguientes ventajas que no están disponibles
en DisplayFormat :
El explorador puede habilitar características de HTML5. Por ejemplo, mostrar un control de calendario, el
símbolo de divisa adecuado según la configuración regional, vínculos de correo electrónico, validación de
entradas del lado cliente, etc.
De manera predeterminada, el explorador representa los datos con el formato correcto según la configuración
regional.
Para obtener más información, vea la documentación de la aplicación auxiliar de etiquetas <entrada>.
Ejecute la aplicación. Vaya a la página de índice de Students. Ya no se muestran las horas. Todas las vistas que usa
el modelo Student muestran la fecha sin hora.

El atributo StringLength
Las reglas de validación de datos y los mensajes de error de validación se pueden especificar con atributos. El
atributo StringLength especifica la longitud mínima y máxima de caracteres que se permite en un campo de
datos. El atributo StringLength también proporciona validación del lado cliente y del lado servidor. El valor
mínimo no influye en el esquema de base de datos.
Actualice el modelo Student con el código siguiente:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models
{
public class Student
{
public int ID { get; set; }
[StringLength(50)]
public string LastName { get; set; }
[StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime EnrollmentDate { get; set; }

public ICollection<Enrollment> Enrollments { get; set; }


}
}

El código anterior limita los nombres a no más de 50 caracteres. El atributo StringLength no impide que un
usuario escriba un espacio en blanco para un nombre. El atributo RegularExpression se usa para aplicar
restricciones a la entrada. Por ejemplo, el código siguiente requiere que el primer carácter sea una letra
mayúscula y el resto de caracteres sean alfabéticos:

[RegularExpression(@"^[A-Z]+[a-zA-Z""'\s-]*$")]

Ejecute la aplicación:
Vaya a la página Students.
Seleccione Create New y escriba un nombre más de 50 caracteres.
Seleccione Create, la validación del lado cliente muestra un mensaje de error.
En el Explorador de objetos de SQL Server, (SSOX) abra el diseñador de tablas de Student haciendo doble clic
en la tabla Student.

La imagen anterior muestra el esquema para la tabla Student . Los campos de nombre tienen tipo nvarchar(MAX)
porque las migraciones no se han ejecutado en la base de datos. Cuando se ejecutan las migraciones más
adelante en este tutorial, los campos de nombre se convierten en nvarchar(50) .
El atributo Column
Los atributos pueden controlar cómo se asignan las clases y propiedades a la base de datos. En esta sección, el
atributo Column se usa para asignar el nombre de la propiedad FirstMidName a "FirstName" en la base de datos.
Cuando se crea la base de datos, los nombres de propiedad en el modelo se usan para los nombres de columna
(excepto cuando se usa el atributo Column ).
El modelo Student usa FirstMidName para el nombre de campo por la posibilidad de que el campo contenga
también un segundo nombre.
Actualice el archivo Models/Student.cs con el siguiente código resaltado:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
public class Student
{
public int ID { get; set; }
[StringLength(50)]
public string LastName { get; set; }
[StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
[Column("FirstName")]
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime EnrollmentDate { get; set; }

public ICollection<Enrollment> Enrollments { get; set; }


}
}

Con el cambio anterior, Student.FirstMidName en la aplicación se asigna a la columna FirstName de la tabla


Student .

La adición del atributo Column cambia el modelo de respaldo de SchoolContext . El modelo que está haciendo la
copia de seguridad de SchoolContext ya no coincide con la base de datos. Si la aplicación se ejecuta antes de
aplicar las migraciones, se genera la siguiente excepción:

SqlException: Invalid column name 'FirstName'.

Para actualizar la base de datos:


Compile el proyecto.
Abra una ventana de comandos en la carpeta del proyecto. Escriba los comandos siguientes para crear una
migración y actualizar la base de datos:

dotnet ef migrations add ColumnFirstName


dotnet ef database update

El comando dotnet ef migrations add ColumnFirstName genera el siguiente mensaje de advertencia:

An operation was scaffolded that may result in the loss of data.


Please review the migration for accuracy.

La advertencia se genera porque los campos de nombre ahora están limitados a 50 caracteres. Si un nombre en
la base de datos tenía más de 50 caracteres, se perderían desde el 51 hasta el último carácter.
Pruebe la aplicación.
Abra la tabla de estudiantes en SSOX:

Antes de aplicar la migración, las columnas de nombre eran de tipo nvarchar(MAX). Las columnas de nombre
ahora son nvarchar(50) . El nombre de columna ha cambiado de FirstMidName a FirstName .

NOTE
En la sección siguiente, la creación de la aplicación en algunas de las fases genera errores del compilador. Las instrucciones
especifican cuándo se debe compilar la aplicación.

Actualizar la entidad Student

Actualice Models/Student.cs con el siguiente código:


using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
public class Student
{
public int ID { get; set; }
[Required]
[StringLength(50)]
[Display(Name = "Last Name")]
public string LastName { get; set; }
[Required]
[StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
[Column("FirstName")]
[Display(Name = "First Name")]
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
[Display(Name = "Enrollment Date")]
public DateTime EnrollmentDate { get; set; }
[Display(Name = "Full Name")]
public string FullName
{
get
{
return LastName + ", " + FirstMidName;
}
}

public ICollection<Enrollment> Enrollments { get; set; }


}
}

El atributo Required
El atributo Required hace que las propiedades de nombre sean campos obligatorios. El atributo Required no es
necesario para los tipos que no aceptan valores NULL, como los tipos de valor ( DateTime , int , double , etc.). Los
tipos que no aceptan valores NULL se tratan automáticamente como campos obligatorios.
El atributo Required se podría reemplazar con un parámetro de longitud mínima en el atributo StringLength :

[Display(Name = "Last Name")]


[StringLength(50, MinimumLength=1)]
public string LastName { get; set; }

El atributo Display
El atributo Display especifica que el título de los cuadros de texto debe ser "First Name", "Last Name", "Full
Name" y "Enrollment Date". Los títulos predeterminados no tenían ningún espacio de división de palabras, por
ejemplo "Lastname".
La propiedad calculada FullName
FullName es una propiedad calculada que devuelve un valor que se crea mediante la concatenación de otras dos
propiedades. No se puede establecer FullName , tiene solo un descriptor de acceso get. No se crea ninguna
columna FullName en la base de datos.

Crear la entidad Instructor


Cree Models/Instructor.cs con el código siguiente:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
public class Instructor
{
public int ID { get; set; }

[Required]
[Display(Name = "Last Name")]
[StringLength(50)]
public string LastName { get; set; }

[Required]
[Column("FirstName")]
[Display(Name = "First Name")]
[StringLength(50)]
public string FirstMidName { get; set; }

[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
[Display(Name = "Hire Date")]
public DateTime HireDate { get; set; }

[Display(Name = "Full Name")]


public string FullName
{
get { return LastName + ", " + FirstMidName; }
}

public ICollection<CourseAssignment> CourseAssignments { get; set; }


public OfficeAssignment OfficeAssignment { get; set; }
}
}

Tenga en cuenta que varias propiedades son las mismas en las entidades Student y Instructor . En el tutorial
Implementación de la herencia, más adelante en esta serie, se refactoriza este código para eliminar la
redundancia.
En una sola línea puede haber varios atributos. Los atributos HireDate pudieron escribirse de la manera
siguiente:

[DataType(DataType.Date),Display(Name = "Hire Date"),DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}",


ApplyFormatInEditMode = true)]

Las propiedades de navegación CourseAssignments y OfficeAssignment


CourseAssignments y OfficeAssignment son propiedades de navegación.
Un instructor puede impartir cualquier número de cursos, por lo que CourseAssignments se define como una
colección.

public ICollection<CourseAssignment> CourseAssignments { get; set; }

Si una propiedad de navegación contiene varias entidades:


Debe ser un tipo de lista, donde se pueden agregar, eliminar y actualizar las entradas.
Los tipos de propiedad de navegación incluyen:
ICollection<T>
List<T>
HashSet<T>

Si se especifica ICollection<T> , EF Core crea una colección HashSet<T> de forma predeterminada.


La entidad CourseAssignment se explica en la sección sobre las relaciones de varios a varios.
Las reglas de negocio de Contoso University establecen que un instructor puede tener, a lo sumo, una oficina. La
propiedad OfficeAssignment contiene una única instancia de OfficeAssignment . OfficeAssignment es NULL si no
se asigna ninguna oficina.

public OfficeAssignment OfficeAssignment { get; set; }

Crear la entidad OfficeAssignment

Cree Models/OfficeAssignment.cs con el código siguiente:

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
public class OfficeAssignment
{
[Key]
public int InstructorID { get; set; }
[StringLength(50)]
[Display(Name = "Office Location")]
public string Location { get; set; }

public Instructor Instructor { get; set; }


}
}

El atributo Key
El atributo [Key] se usa para identificar una propiedad como la clave principal (PK) cuando el nombre de
propiedad es diferente de classnameID o ID.
Hay una relación de uno a cero o uno entre las entidades Instructor y OfficeAssignment . Solo existe una
asignación de oficina en relación con el instructor a la que está asignada. La clave principal de OfficeAssignment
también es la clave externa (FK) para la entidad Instructor . EF Core no reconoce automáticamente
InstructorID como la clave principal de OfficeAssignment porque:

InstructorID no sigue la convención de nomenclatura de ID o classnameID.

Por tanto, se usa el atributo Key para identificar InstructorID como la clave principal:

[Key]
public int InstructorID { get; set; }

De forma predeterminada, EF Core trata la clave como no generada por la base de datos porque la columna es
para una relación de identificación.
La propiedad de navegación Instructor
La propiedad de navegación OfficeAssignment para la entidad Instructor acepta valores NULL porque:
Los tipos de referencia, como las clases, aceptan valores NULL.
Un instructor podría no tener una asignación de oficina.
La entidad OfficeAssignment tiene una propiedad de navegación Instructor que no acepta valores NULL
porque:
InstructorIDno acepta valores NULL.
Una asignación de oficina no puede existir sin un instructor.
Cuando una entidad Instructor tiene una entidad OfficeAssignment relacionada, cada entidad tiene una
referencia a la otra en su propiedad de navegación.
El atributo [Required] puede aplicarse a la propiedad de navegación Instructor :

[Required]
public Instructor Instructor { get; set; }

El código anterior especifica que debe haber un instructor relacionado. El código anterior no es necesario porque
la clave externa InstructorID , que también es la clave principal, no acepta valores NULL.

Modificar la entidad Course

Actualice Models/Course.cs con el siguiente código:


using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
public class Course
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }

[StringLength(50, MinimumLength = 3)]


public string Title { get; set; }

[Range(0, 5)]
public int Credits { get; set; }

public int DepartmentID { get; set; }

public Department Department { get; set; }


public ICollection<Enrollment> Enrollments { get; set; }
public ICollection<CourseAssignment> CourseAssignments { get; set; }
}
}

La entidad Course tiene una propiedad de clave externa (FK) DepartmentID . DepartmentID apunta a la entidad
relacionada Department . La entidad Course tiene una propiedad de navegación Department .
EF Core no requiere una propiedad de clave externa para un modelo de datos cuando el modelo tiene una
propiedad de navegación para una entidad relacionada.
EF Core crea automáticamente claves externas en la base de datos siempre que se necesiten. EF Core crea
propiedades paralelas para las claves externas creadas automáticamente. Tener la clave externa en el modelo de
datos puede hacer que las actualizaciones sean más sencillas y eficaces. Por ejemplo, considere la posibilidad de
un modelo donde la propiedad de la clave externa DepartmentID no está incluida. Cuando se captura una entidad
de curso para editar:
La entidad Department es NULL si no se carga explícitamente.
Para actualizar la entidad Course, la entidad Department debe capturarse en primer lugar.

Cuando se incluye la propiedad de clave externa DepartmentID en el modelo de datos, no es necesario capturar la
entidad Department antes de una actualización.
El atributo DatabaseGenerated
El atributo [DatabaseGenerated(DatabaseGeneratedOption.None)] especifica que la aplicación proporciona la clave
principal, en lugar de generarla la base de datos.

[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }

De forma predeterminada, EF Core da por supuesto que la base de datos genera valores de clave principal. Los
valores de clave principal generados por la base de datos suelen ser el mejor método. Para las entidades Course ,
el usuario especifica la clave principal. Por ejemplo, un número de curso como una serie de 1000 para el
departamento de matemáticas, una serie de 2000 para el departamento de inglés.
También se puede usar el atributo DatabaseGenerated para generar valores predeterminados. Por ejemplo, la base
de datos puede generar automáticamente un campo de fecha para registrar la fecha en que se crea o actualiza
una fila. Para obtener más información, vea Propiedades generadas.
Propiedades de clave externa y de navegación
Las propiedades de clave externa (FK) y las de navegación de la entidad Course reflejan las relaciones siguientes:
Un curso se asigna a un departamento, por lo que hay una clave externa DepartmentID y una propiedad de
navegación Department .

public int DepartmentID { get; set; }


public Department Department { get; set; }

Un curso puede tener cualquier número de alumnos inscritos en él, por lo que la propiedad de navegación
Enrollments es una colección:

public ICollection<Enrollment> Enrollments { get; set; }

Un curso puede ser impartido por varios instructores, por lo que la propiedad de navegación CourseAssignments
es una colección:

public ICollection<CourseAssignment> CourseAssignments { get; set; }

CourseAssignment se explica más adelante.

Crear la entidad Department

Cree Models/Department.cs con el código siguiente:


using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
public class Department
{
public int DepartmentID { get; set; }

[StringLength(50, MinimumLength = 3)]


public string Name { get; set; }

[DataType(DataType.Currency)]
[Column(TypeName = "money")]
public decimal Budget { get; set; }

[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
[Display(Name = "Start Date")]
public DateTime StartDate { get; set; }

public int? InstructorID { get; set; }

public Instructor Administrator { get; set; }


public ICollection<Course> Courses { get; set; }
}
}

El atributo Column
Anteriormente se usó el atributo Column para cambiar la asignación de nombres de columna. En el código de la
entidad Department , se usó el atributo Column para cambiar la asignación de tipos de datos de SQL. La columna
Budget se define mediante el tipo money de SQL Server en la base de datos:

[Column(TypeName="money")]
public decimal Budget { get; set; }

Por lo general, la asignación de columnas no es necesaria. EF Core generalmente elige el tipo de datos de SQL
Server apropiado en función del tipo CLR para la propiedad. El tipo CLR decimal se asigna a un tipo decimal de
SQL Server. Budget es para la divisa, y el tipo de datos money es más adecuado para la divisa.
Propiedades de clave externa y de navegación
Las propiedades de clave externa y de navegación reflejan las relaciones siguientes:
Un departamento puede tener o no un administrador.
Un administrador siempre es un instructor. Por lo tanto, la propiedad InstructorID se incluye como la clave
externa para la entidad Instructor .
La propiedad de navegación se denomina Administrator pero contiene una entidad Instructor :

public int? InstructorID { get; set; }


public Instructor Administrator { get; set; }

El signo de interrogación (?) en el código anterior especifica que la propiedad acepta valores NULL.
Un departamento puede tener varios cursos, por lo que hay una propiedad de navegación Courses:
public ICollection<Course> Courses { get; set; }

Nota: Por convención, EF Core permite la eliminación en cascada para las claves externas que no aceptan valores
NULL y para las relaciones de varios a varios. La eliminación en cascada puede dar lugar a reglas de eliminación
en cascada circular. Las reglas de eliminación en cascada circular provocan una excepción cuando se agrega una
migración.
Por ejemplo, si no se ha definido que la propiedad Department.InstructorID acepta valores NULL:
EF Core configura una regla de eliminación en cascada para eliminar el instructor cuando se elimine el
departamento.
Eliminar el instructor cuando se elimine el departamento no es el comportamiento previsto.
Si las reglas de negocios requieren que la propiedad InstructorID no acepte valores NULL, use la siguiente
instrucción de API fluida:

modelBuilder.Entity<Department>()
.HasOne(d => d.Administrator)
.WithMany()
.OnDelete(DeleteBehavior.Restrict)

El código anterior deshabilita la eliminación en cascada en la relación de instructor y departamento.

Actualizar la entidad Enrollment


Un registro de inscripción es para un curso realizado por un alumno.

Actualice Models/Enrollment.cs con el siguiente código:


using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
public enum Grade
{
A, B, C, D, F
}

public class Enrollment


{
public int EnrollmentID { get; set; }
public int CourseID { get; set; }
public int StudentID { get; set; }
[DisplayFormat(NullDisplayText = "No grade")]
public Grade? Grade { get; set; }

public Course Course { get; set; }


public Student Student { get; set; }
}
}

Propiedades de clave externa y de navegación


Las propiedades de clave externa y de navegación reflejan las relaciones siguientes:
Un registro de inscripción es para un curso, por lo que hay una propiedad de clave externa CourseID y una
propiedad de navegación Course :

public int CourseID { get; set; }


public Course Course { get; set; }

Un registro de inscripción es para un alumno, por lo que hay una propiedad de clave externa StudentID y una
propiedad de navegación Student :

public int StudentID { get; set; }


public Student Student { get; set; }

Relaciones Varios a Varios


Hay una relación de varios a varios entre las entidades Student y Course . La entidad Enrollment funciona como
una tabla combinada varios a varios con carga útil en la base de datos. "Con carga útil" significa que la tabla
Enrollment contiene datos adicionales, además de claves externas de las tablas combinadas (en este caso, la clave
principal y Grade ).
En la ilustración siguiente se muestra el aspecto de estas relaciones en un diagrama de entidades. (Este diagrama
se ha generado mediante EF Power Tools para EF 6.x. Crear el diagrama no forma parte del tutorial).
Cada línea de relación tiene un 1 en un extremo y un asterisco (*) en el otro, para indicar una relación uno a
varios.
Si la tabla Enrollment no incluyera información de calificaciones, solo tendría que contener las dos claves
externas ( CourseID y StudentID ). Una tabla combinada de varios a varios sin carga útil se suele denominar una
tabla combinada pura (PJT).
Las entidades Instructor y Course tienen una relación de varios a varios con una tabla combinada pura.
Nota: EF 6.x es compatible con las tablas de combinación implícitas para relaciones de varios a varios, pero EF
Core no. Para obtener más información, consulte Many-to-many relationships in EF Core 2.0 (Relaciones de
varios a varios en EF Core 2.0).

La entidad CourseAssignment

Cree Models/CourseAssignment.cs con el código siguiente:


using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
public class CourseAssignment
{
public int InstructorID { get; set; }
public int CourseID { get; set; }
public Instructor Instructor { get; set; }
public Course Course { get; set; }
}
}

Relación Instructor-to -Courses

La relación de varios a varios Instructor-to-Courses:


Requiere una tabla de combinación que debe estar representada por un conjunto de entidades.
Es una tabla combinada pura (tabla sin carga útil).
Es común denominar una entidad de combinación EntityName1EntityName2 . Por ejemplo, la tabla de combinación
Instructor-to-Courses usando este patrón es CourseInstructor . Pero se recomienda usar un nombre que describa
la relación.
Los modelos de datos empiezan de manera sencilla y crecen. Las tablas combinadas sin carga útil (PJT)
evolucionan con frecuencia para incluir la carga útil. A partir de un nombre de entidad descriptivo, no es
necesario cambiar el nombre cuando la tabla combinada cambia. Idealmente, la entidad de combinación tendrá
su propio nombre natural (posiblemente una sola palabra) en el dominio de empresa. Por ejemplo, Books y
Customers podrían vincularse a través de una entidad combinada denominada Ratings. Para la relación de varios
a varios Instructor-to-Courses, se prefiere CourseAssignment a CourseInstructor .
Clave compuesta
Las claves externas no aceptan valores NULL. Las dos claves externas en CourseAssignment ( InstructorID y
CourseID ) juntas identifican de forma única cada fila de la tabla CourseAssignment . CourseAssignment no requiere
una clave principal dedicada. Las propiedades InstructorID y CourseID funcionan como una clave principal
compuesta. La única manera de especificar claves principales compuestas en EF Core es con la API fluida. La
sección siguiente muestra cómo configurar la clave principal compuesta.
La clave compuesta asegura que:
Se permiten varias filas para un curso.
Se permiten varias filas para un instructor.
No se permiten varias filas para el mismo instructor y curso.
La entidad de combinación Enrollment define su propia clave principal, por lo que este tipo de duplicados son
posibles. Para evitar los duplicados:
Agregue un índice único en los campos de clave externa, o
Configure Enrollment con una clave compuesta principal similar a CourseAssignment . Para obtener más
información, vea Índices.

Actualizar el contexto de la base de datos


Agregue el código resaltado siguiente a Data/SchoolContext.cs:

using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity.Data
{
public class SchoolContext : DbContext
{
public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
{
}

public DbSet<Course> Courses { get; set; }


public DbSet<Enrollment> Enrollments { get; set; }
public DbSet<Student> Students { get; set; }
public DbSet<Department> Departments { get; set; }
public DbSet<Instructor> Instructors { get; set; }
public DbSet<OfficeAssignment> OfficeAssignments { get; set; }
public DbSet<CourseAssignment> CourseAssignments { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)


{
modelBuilder.Entity<Course>().ToTable("Course");
modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
modelBuilder.Entity<Student>().ToTable("Student");
modelBuilder.Entity<Department>().ToTable("Department");
modelBuilder.Entity<Instructor>().ToTable("Instructor");
modelBuilder.Entity<OfficeAssignment>().ToTable("OfficeAssignment");
modelBuilder.Entity<CourseAssignment>().ToTable("CourseAssignment");

modelBuilder.Entity<CourseAssignment>()
.HasKey(c => new { c.CourseID, c.InstructorID });
}
}
}

El código anterior agrega las nuevas entidades y configura la clave principal compuesta de la entidad
CourseAssignment .
Alternativa de la API fluida a los atributos
El método OnModelCreating del código anterior usa la API fluida para configurar el comportamiento de EF Core.
La API se denomina "fluida" porque a menudo se usa para encadenar una serie de llamadas de método en una
única instrucción. El código siguiente es un ejemplo de la API fluida:

protected override void OnModelCreating(ModelBuilder modelBuilder)


{
modelBuilder.Entity<Blog>()
.Property(b => b.Url)
.IsRequired();
}

En este tutorial, la API fluida se usa solo para la asignación de base de datos que no se puede realizar con
atributos. Pero la API fluida puede especificar casi todas las reglas de formato, validación y asignación que se
pueden realizar mediante el uso de atributos.
Algunos atributos como MinimumLength no se pueden aplicar con la API fluida. MinimumLength no cambia el
esquema, solo aplica una regla de validación de longitud mínima.
Algunos desarrolladores prefieren usar la API fluida exclusivamente para mantener "limpias" las clases de
entidad. Se pueden mezclar atributos y la API fluida. Hay algunas configuraciones que solo se pueden realizar con
la API fluida (especificando una clave principal compuesta). Hay algunas configuraciones que solo se pueden
realizar con atributos ( MinimumLength ). La práctica recomendada para el uso de atributos o API fluida:
Elija uno de estos dos enfoques.
Use el enfoque elegido de forma tan coherente como sea posible.
Algunos de los atributos utilizados en este tutorial se usan para:
Solo validación (por ejemplo, MinimumLength ).
Solo configuración de EF Core (por ejemplo, HasKey ).
Validación y configuración de EF Core (por ejemplo, [StringLength(50)] ).
Para obtener más información sobre la diferencia entre los atributos y la API fluida, vea Métodos de
configuración.

Diagrama de entidades en el que se muestran las relaciones


En la siguiente ilustración se muestra el diagrama creado por EF Power Tools para el modelo School completado.
El diagrama anterior muestra:
Varias líneas de relación uno a varios (1 a *).
La línea de relación de uno a cero o uno (1 a 0..1) entre las entidades Instructor y OfficeAssignment .
La línea de relación de cero o uno o varios (0..1 a *) entre las entidades Instructor y Department .

Inicializar la base de datos con datos de prueba


Actualice el código en Data/DbInitializer.cs:

using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using ContosoUniversity.Models;

namespace ContosoUniversity.Data
{
public static class DbInitializer
{
public static void Initialize(SchoolContext context)
{
//context.Database.EnsureCreated();
// Look for any students.
if (context.Students.Any())
{
return; // DB has been seeded
}

var students = new Student[]


{
new Student { FirstMidName = "Carson", LastName = "Alexander",
EnrollmentDate = DateTime.Parse("2010-09-01") },
new Student { FirstMidName = "Meredith", LastName = "Alonso",
EnrollmentDate = DateTime.Parse("2012-09-01") },
new Student { FirstMidName = "Arturo", LastName = "Anand",
EnrollmentDate = DateTime.Parse("2013-09-01") },
new Student { FirstMidName = "Gytis", LastName = "Barzdukas",
EnrollmentDate = DateTime.Parse("2012-09-01") },
new Student { FirstMidName = "Yan", LastName = "Li",
EnrollmentDate = DateTime.Parse("2012-09-01") },
new Student { FirstMidName = "Peggy", LastName = "Justice",
EnrollmentDate = DateTime.Parse("2011-09-01") },
new Student { FirstMidName = "Laura", LastName = "Norman",
EnrollmentDate = DateTime.Parse("2013-09-01") },
new Student { FirstMidName = "Nino", LastName = "Olivetto",
EnrollmentDate = DateTime.Parse("2005-09-01") }
};

foreach (Student s in students)


{
context.Students.Add(s);
}
context.SaveChanges();

var instructors = new Instructor[]


{
new Instructor { FirstMidName = "Kim", LastName = "Abercrombie",
HireDate = DateTime.Parse("1995-03-11") },
new Instructor { FirstMidName = "Fadi", LastName = "Fakhouri",
HireDate = DateTime.Parse("2002-07-06") },
new Instructor { FirstMidName = "Roger", LastName = "Harui",
HireDate = DateTime.Parse("1998-07-01") },
new Instructor { FirstMidName = "Candace", LastName = "Kapoor",
HireDate = DateTime.Parse("2001-01-15") },
new Instructor { FirstMidName = "Roger", LastName = "Zheng",
HireDate = DateTime.Parse("2004-02-12") }
};

foreach (Instructor i in instructors)


{
context.Instructors.Add(i);
}
context.SaveChanges();

var departments = new Department[]


{
new Department { Name = "English", Budget = 350000,
StartDate = DateTime.Parse("2007-09-01"),
InstructorID = instructors.Single( i => i.LastName == "Abercrombie").ID },
new Department { Name = "Mathematics", Budget = 100000,
StartDate = DateTime.Parse("2007-09-01"),
InstructorID = instructors.Single( i => i.LastName == "Fakhouri").ID },
new Department { Name = "Engineering", Budget = 350000,
StartDate = DateTime.Parse("2007-09-01"),
InstructorID = instructors.Single( i => i.LastName == "Harui").ID },
new Department { Name = "Economics", Budget = 100000,
StartDate = DateTime.Parse("2007-09-01"),
InstructorID = instructors.Single( i => i.LastName == "Kapoor").ID }
};

foreach (Department d in departments)


foreach (Department d in departments)
{
context.Departments.Add(d);
}
context.SaveChanges();

var courses = new Course[]


{
new Course {CourseID = 1050, Title = "Chemistry", Credits = 3,
DepartmentID = departments.Single( s => s.Name == "Engineering").DepartmentID
},
new Course {CourseID = 4022, Title = "Microeconomics", Credits = 3,
DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID
},
new Course {CourseID = 4041, Title = "Macroeconomics", Credits = 3,
DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID
},
new Course {CourseID = 1045, Title = "Calculus", Credits = 4,
DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID
},
new Course {CourseID = 3141, Title = "Trigonometry", Credits = 4,
DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID
},
new Course {CourseID = 2021, Title = "Composition", Credits = 3,
DepartmentID = departments.Single( s => s.Name == "English").DepartmentID
},
new Course {CourseID = 2042, Title = "Literature", Credits = 4,
DepartmentID = departments.Single( s => s.Name == "English").DepartmentID
},
};

foreach (Course c in courses)


{
context.Courses.Add(c);
}
context.SaveChanges();

var officeAssignments = new OfficeAssignment[]


{
new OfficeAssignment {
InstructorID = instructors.Single( i => i.LastName == "Fakhouri").ID,
Location = "Smith 17" },
new OfficeAssignment {
InstructorID = instructors.Single( i => i.LastName == "Harui").ID,
Location = "Gowan 27" },
new OfficeAssignment {
InstructorID = instructors.Single( i => i.LastName == "Kapoor").ID,
Location = "Thompson 304" },
};

foreach (OfficeAssignment o in officeAssignments)


{
context.OfficeAssignments.Add(o);
}
context.SaveChanges();

var courseInstructors = new CourseAssignment[]


{
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
InstructorID = instructors.Single(i => i.LastName == "Kapoor").ID
},
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
InstructorID = instructors.Single(i => i.LastName == "Harui").ID
},
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Microeconomics" ).CourseID,
InstructorID = instructors.Single(i => i.LastName == "Zheng").ID
},
},
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Macroeconomics" ).CourseID,
InstructorID = instructors.Single(i => i.LastName == "Zheng").ID
},
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Calculus" ).CourseID,
InstructorID = instructors.Single(i => i.LastName == "Fakhouri").ID
},
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Trigonometry" ).CourseID,
InstructorID = instructors.Single(i => i.LastName == "Harui").ID
},
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Composition" ).CourseID,
InstructorID = instructors.Single(i => i.LastName == "Abercrombie").ID
},
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Literature" ).CourseID,
InstructorID = instructors.Single(i => i.LastName == "Abercrombie").ID
},
};

foreach (CourseAssignment ci in courseInstructors)


{
context.CourseAssignments.Add(ci);
}
context.SaveChanges();

var enrollments = new Enrollment[]


{
new Enrollment {
StudentID = students.Single(s => s.LastName == "Alexander").ID,
CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
Grade = Grade.A
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Alexander").ID,
CourseID = courses.Single(c => c.Title == "Microeconomics" ).CourseID,
Grade = Grade.C
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Alexander").ID,
CourseID = courses.Single(c => c.Title == "Macroeconomics" ).CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Alonso").ID,
CourseID = courses.Single(c => c.Title == "Calculus" ).CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Alonso").ID,
CourseID = courses.Single(c => c.Title == "Trigonometry" ).CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Alonso").ID,
CourseID = courses.Single(c => c.Title == "Composition" ).CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Anand").ID,
CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Anand").ID,
CourseID = courses.Single(c => c.Title == "Microeconomics").CourseID,
Grade = Grade.B
},
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Barzdukas").ID,
CourseID = courses.Single(c => c.Title == "Chemistry").CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Li").ID,
CourseID = courses.Single(c => c.Title == "Composition").CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Justice").ID,
CourseID = courses.Single(c => c.Title == "Literature").CourseID,
Grade = Grade.B
}
};

foreach (Enrollment e in enrollments)


{
var enrollmentInDataBase = context.Enrollments.Where(
s =>
s.Student.ID == e.StudentID &&
s.Course.CourseID == e.CourseID).SingleOrDefault();
if (enrollmentInDataBase == null)
{
context.Enrollments.Add(e);
}
}
context.SaveChanges();
}
}
}

El código anterior proporciona datos de inicialización para las nuevas entidades. La mayor parte de este código
crea objetos de entidad y carga los datos de ejemplo. Los datos de ejemplo se usan para pruebas. El código
anterior crea las siguientes relaciones de varios a varios:
Enrollments
CourseAssignment

Nota: EF Core 2.1 admitirá la propagación de datos.

Agregar una migración


Compile el proyecto. Abra una ventana de comandos en la carpeta de proyecto y escriba el siguiente comando:

dotnet ef migrations add ComplexDataModel

El comando anterior muestra una advertencia sobre la posible pérdida de datos.

An operation was scaffolded that may result in the loss of data.


Please review the migration for accuracy.
Done. To undo this action, use 'ef migrations remove'

Si se ejecuta el comando database update , se genera el error siguiente:

The ALTER TABLE statement conflicted with the FOREIGN KEY constraint
"FK_dbo.Course_dbo.Department_DepartmentID". The conflict occurred in
database "ContosoUniversity", table "dbo.Department", column 'DepartmentID'.
Cuando se ejecutan migraciones con datos existentes, puede haber restricciones de clave externa que no se
cumplen con los datos existentes. Para este tutorial, se crea una base de datos, por lo que no hay ninguna
infracción de restricciones de clave externa. Vea la sección Corregir las restricciones de claves externas con datos
heredados para obtener instrucciones sobre cómo corregir las infracciones de clave externa en la base de datos
actual.

Cambiar la cadena de conexión y actualizar la base de datos


El código en la DbInitializer actualizada agrega los datos de inicialización para las nuevas entidades. Para
obligar a EF Core a crear una base de datos vacía:
Cambie el nombre de la cadena de conexión de la base de datos en appSettings.JSON a
ContosoUniversity3. El nuevo nombre debe ser un nombre que no se haya usado en el equipo.

{
"ConnectionStrings": {
"DefaultConnection": "Server=
(localdb)\\mssqllocaldb;Database=ContosoUniversity3;Trusted_Connection=True;MultipleActiveResultSets=tr
ue"
},

O bien, elimine la base de datos mediante:


Explorador de objetos de SQL Server (SSOX).
El comando de la CLI database drop :

dotnet ef database drop

Ejecute database update en la ventana de comandos:

dotnet ef database update

El comando anterior ejecuta todas las migraciones.


Ejecute la aplicación. Ejecutar la aplicación ejecuta el método DbInitializer.Initialize .
DbInitializer.Initialize rellena la base de datos nueva.

Abra la base de datos en SSOX:


Expanda el nodo Tablas. Se muestran las tablas creadas.
Si anteriormente se abrió SSOX, haga clic en el botón Actualizar.
Examine la tabla CourseAssignment:
Haga clic con el botón derecho en la tabla CourseAssignment y seleccione Ver datos.
Compruebe que la tabla CourseAssignment contiene datos.

Corregir las restricciones de claves externas con datos heredados


Esta sección es opcional.
Cuando se ejecutan migraciones con datos existentes, puede haber restricciones de clave externa que no se
cumplen con los datos existentes. Con los datos de producción, se deben realizar algunos pasos para migrar los
datos existentes. En esta sección se proporciona un ejemplo de corrección de las infracciones de restricción de
clave externa. No realice estos cambios de código sin hacer una copia de seguridad. No realice estos cambios de
código si realizó la sección anterior y actualizó la base de datos.
El archivo {marca_de_tiempo }_ComplexDataModel.cs contiene el código siguiente:

migrationBuilder.AddColumn<int>(
name: "DepartmentID",
table: "Course",
type: "int",
nullable: false,
defaultValue: 0);

El código anterior agrega una clave externa DepartmentID que acepta valores NULL a la tabla Course . La base de
datos del tutorial anterior contiene filas en Course , por lo que no se puede actualizar esa tabla mediante
migraciones.
Para realizar la migración de ComplexDataModel , trabaje con los datos existentes:
Cambie el código para asignar a la nueva columna ( DepartmentID ) un valor predeterminado.
Cree un departamento falso denominado "Temp" para que actúe como el departamento predeterminado.
Corregir las restricciones de clave externa
Actualice el método Up de las clases ComplexDataModel :
Abra el archivo {marca_de_tiempo }_ComplexDataModel.cs.
Convierta en comentario la línea de código que agrega la columna DepartmentID a la tabla Course .

migrationBuilder.AlterColumn<string>(
name: "Title",
table: "Course",
maxLength: 50,
nullable: true,
oldClrType: typeof(string),
oldNullable: true);

//migrationBuilder.AddColumn<int>(
// name: "DepartmentID",
// table: "Course",
// nullable: false,
// defaultValue: 0);

Agregue el código resaltado siguiente. El nuevo código va después del bloque .CreateTable( name: "Department" :
[!code-csharp]
Con los cambios anteriores, las filas Course existentes estarán relacionadas con el departamento "Temp" después
de ejecutar el método ComplexDataModel de Up .
Una aplicación de producción debería:
Incluir código o scripts para agregar filas de Department y filas de Course relacionadas a las nuevas filas de
Department .
No use el departamento "Temp" o el valor predeterminado de Course.DepartmentID .
El siguiente tutorial trata los datos relacionados.

A N T E R IO R S IG U IE N T E
Páginas de Razor con EF Core en ASP.NET Core:
Lectura de datos relacionados (6 de 8)
19/06/2018 • 26 minutes to read • Edit Online

Por Tom Dykstra, Jon P Smith y Rick Anderson


La aplicación web Contoso University muestra cómo crear aplicaciones web de las páginas de Razor con EF Core
y Visual Studio. Para obtener información sobre la serie de tutoriales, consulte el primer tutorial.
En este tutorial, se leen y se muestran datos relacionados. Los datos relacionados son los que EF Core carga en
las propiedades de navegación.
Si experimenta problemas que no puede resolver, descargue la aplicación completada para esta fase.
En las ilustraciones siguientes se muestran las páginas completadas para este tutorial:
Carga diligente, explícita y diferida de datos relacionados
EF Core puede cargar datos relacionados en las propiedades de navegación de una entidad de varias maneras:
Carga diligente. La carga diligente es cuando una consulta para un tipo de entidad también carga las
entidades relacionadas. Cuando se lee la entidad, se recuperan sus datos relacionados. Esto normalmente
da como resultado una única consulta de combinación en la que se recuperan todos los datos que se
necesitan. EF Core emitirá varias consultas para algunos tipos de carga diligente. La emisión de varias
consultas puede ser más eficaz de lo que eran algunas consultas de EF6 cuando había una sola consulta. La
carga diligente se especifica con los métodos Include y ThenInclude .

La carga diligente envía varias consultas cuando se incluye una propiedad de navegación de colección:
Una consulta para la consulta principal
Una consulta para cada colección "perimetral" en el árbol de la carga.
Separe las consultas con Load : los datos se pueden recuperar en distintas consultas y EF Core "corrige" las
propiedades de navegación. "Corregir" significa que EF Core rellena automáticamente las propiedades de
navegación. Separar las consultas con Load es más parecido a la carga explícita que a la carga diligente.

Nota: EF Core corrige automáticamente las propiedades de navegación para todas las entidades que se
cargaron previamente en la instancia del contexto. Incluso si los datos de una propiedad de navegación no
se incluyen explícitamente, es posible que la propiedad se siga rellenando si algunas o todas las entidades
relacionadas se cargaron previamente.
Carga explícita. Cuando la entidad se lee por primera vez, no se recuperan datos relacionados. Se debe
escribir código para recuperar los datos relacionados cuando sea necesario. La carga explícita con
consultas independientes da como resultado varias consultas que se envían a la base de datos. Con la
carga explícita, el código especifica las propiedades de navegación que se van a cargar. Use el método
Load para realizar la carga explícita. Por ejemplo:

Carga diferida. Actualmente EF Core no admite la carga diferida. Cuando la entidad se lee por primera vez,
no se recuperan datos relacionados. La primera vez que se obtiene acceso a una propiedad de navegación,
se recuperan automáticamente los datos necesarios para esa propiedad de navegación. Cada vez que se
obtiene acceso a una propiedad de navegación, se envía una consulta a la base de datos.
El operador Select solo carga los datos relacionados necesarios.

Crear una página de cursos en la que se muestre el nombre de


departamento
La entidad Course incluye una propiedad de navegación que contiene la entidad Department . La entidad
Department contiene el departamento al que se asigna el curso.
Para mostrar el nombre del departamento asignado en una lista de cursos:
Obtenga la propiedad Name desde la entidad Department .
La entidad Department procede de la propiedad de navegación Course.Department .

Aplicar scaffolding al modelo de Course


Salga de Visual Studio.
Abra una ventana de comandos en el directorio del proyecto (el directorio que contiene los archivos
Program.cs, Startup.cs y .csproj).
Ejecute el siguiente comando:

dotnet aspnet-codegenerator razorpage -m Course -dc SchoolContext -udl -outDir Pages\Courses --


referenceScriptLibraries

El comando anterior aplica scaffolding al modelo Course . Abra el proyecto en Visual Studio.
Compile el proyecto. La compilación genera errores similares a los siguientes:
1>Pages/Courses/Index.cshtml.cs(26,37,26,43): error CS1061: 'SchoolContext' does not contain a definition for
'Course' and no extension method 'Course' accepting a first argument of type 'SchoolContext' could be found
(are you missing a using directive or an assembly reference?)

Cambie globalmente _context.Course por _context.Courses (es decir, agregue una "s" a Course ). Se encuentran
y actualizan siete repeticiones.
Abra Pages/Courses/Index.cshtml.cs y examine el método OnGetAsync . El motor de scaffolding especificado
realiza la carga diligente de la propiedad de navegación Department . El método Include especifica la carga
diligente.
Ejecute la aplicación y haga clic en el vínculo Courses. En la columna Department se muestra el DepartmentID , lo
que no resulta útil.
Actualice el método OnGetAsync con el código siguiente:

public async Task OnGetAsync()


{
Course = await _context.Courses
.Include(c => c.Department)
.AsNoTracking()
.ToListAsync();
}

El código anterior agrega AsNoTracking . AsNoTracking mejora el rendimiento porque no se realiza el seguimiento
de las entidades devueltas. No se realiza el seguimiento de las entidades porque no se actualizan en el contexto
actual.
Actualice Pages/Courses/Index.cshtml con el marcado resaltado siguiente:
@page
@model ContosoUniversity.Pages.Courses.IndexModel
@{
ViewData["Title"] = "Courses";
}

<h2>Courses</h2>

<p>
<a asp-page="TestCreate">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Course[0].CourseID)
</th>
<th>
@Html.DisplayNameFor(model => model.Course[0].Title)
</th>
<th>
@Html.DisplayNameFor(model => model.Course[0].Credits)
</th>
<th>
@Html.DisplayNameFor(model => model.Course[0].Department)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Course)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.CourseID)
</td>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.Credits)
</td>
<td>
@Html.DisplayFor(modelItem => item.Department.Name)
</td>
<td>
<a asp-page="./Edit" asp-route-id="@item.CourseID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.CourseID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.CourseID">Delete</a>
</td>
</tr>
}
</tbody>
</table>

Se han realizado los cambios siguientes en el código con scaffolding:


Ha cambiado el título de Index a Courses.
Ha agregado una columna Number en la que se muestra el valor de propiedad CourseID . De forma
predeterminada, las claves principales no tienen scaffolding porque normalmente no tienen sentido para
los usuarios finales. Pero en este caso, la clave principal es significativa.
Ha cambiado la columna Department para mostrar el nombre del departamento. El código muestra la
propiedad Name de la entidad Department que se carga en la propiedad de navegación Department :
@Html.DisplayFor(modelItem => item.Department.Name)

Ejecute la aplicación y haga clic en la pestaña Courses para ver la lista con los nombres de departamento.

Carga de datos relacionados con Select


El método OnGetAsync carga los datos relacionados con el método Include :

public async Task OnGetAsync()


{
Course = await _context.Courses
.Include(c => c.Department)
.AsNoTracking()
.ToListAsync();
}

El operador Select solo carga los datos relacionados necesarios. Para elementos individuales, como el
Department.Name , se usa INNER JOIN de SQL. En las colecciones, se usa otro acceso de base de datos, como
también hace el operador Include en las colecciones.
En el código siguiente se cargan los datos relacionados con el método Select :

public IList<CourseViewModel> CourseVM { get; set; }

public async Task OnGetAsync()


{
CourseVM = await _context.Courses
.Select(p => new CourseViewModel
{
CourseID = p.CourseID,
Title = p.Title,
Credits = p.Credits,
DepartmentName = p.Department.Name
}).ToListAsync();
}

El CourseViewModel :
public class CourseViewModel
{
public int CourseID { get; set; }
public string Title { get; set; }
public int Credits { get; set; }
public string DepartmentName { get; set; }
}

Vea IndexSelect.cshtml e IndexSelect.cshtml.cs para obtener un ejemplo completo.

Crear una página de instructores en la que se muestran los cursos y las


inscripciones
En esta sección, se crea la página de instructores.
En esta página se leen y muestran los datos relacionados de las maneras siguientes:
En la lista de instructores se muestran datos relacionados de la entidad OfficeAssignment (Office en la imagen
anterior). Las entidades Instructor y OfficeAssignment se encuentran en una relación de uno a cero o uno.
Para las entidades OfficeAssignment se usa la carga diligente. Normalmente la carga diligente es más eficaz
cuando es necesario mostrar los datos relacionados. En este caso, se muestran las asignaciones de oficina para
los instructores.
Cuando el usuario selecciona un instructor (Harui en la imagen anterior), se muestran las entidades Course
relacionadas. Las entidades Instructor y Course se encuentran en una relación de varios a varios. La carga
diligente se usa con las entidades Course y sus entidades Department relacionadas. En este caso, es posible
que las consultas independientes sean más eficaces porque solo se necesitan cursos para el instructor
seleccionado. En este ejemplo se muestra cómo usar la carga diligente para propiedades de navegación en
entidades que se encuentran en propiedades de navegación.
Cuando el usuario selecciona un curso (Chemistry [Química] en la imagen anterior), se muestran los datos
relacionados de la entidad Enrollments . En la imagen anterior, se muestra el nombre del alumno y la
calificación. Las entidades Course y Enrollment se encuentran en una relación uno a varios.
Crear un modelo de vista para la vista de índice de instructores
En la página Instructors se muestran datos de tres tablas diferentes. Se crea un modelo de vista que incluye las
tres entidades que representan las tres tablas.
En la carpeta SchoolViewModels, cree InstructorIndexData.cs con el código siguiente:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Models.SchoolViewModels
{
public class InstructorIndexData
{
public IEnumerable<Instructor> Instructors { get; set; }
public IEnumerable<Course> Courses { get; set; }
public IEnumerable<Enrollment> Enrollments { get; set; }
}
}

Aplicar scaffolding al modelo de Instructor


Salga de Visual Studio.
Abra una ventana de comandos en el directorio del proyecto (el directorio que contiene los archivos
Program.cs, Startup.cs y .csproj).
Ejecute el siguiente comando:

dotnet aspnet-codegenerator razorpage -m Instructor -dc SchoolContext -udl -outDir Pages\Instructors --


referenceScriptLibraries

El comando anterior aplica scaffolding al modelo Instructor . Abra el proyecto en Visual Studio.
Compile el proyecto. La compilación genera errores.
Cambie globalmente _context.Instructor por _context.Instructors (es decir, agregue una "s" a Instructor ). Se
encuentran y actualizan siete repeticiones.
Ejecute la aplicación y vaya a la página de instructores.
Reemplace Pages/Instructors/Index.cshtml.cs con el código siguiente:
using ContosoUniversity.Models;
using ContosoUniversity.Models.SchoolViewModels; // Add VM
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Instructors
{
public class IndexModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;

public IndexModel(ContosoUniversity.Data.SchoolContext context)


{
_context = context;
}

public InstructorIndexData Instructor { get; set; }


public int InstructorID { get; set; }

public async Task OnGetAsync(int? id)


{
Instructor = new InstructorIndexData();
Instructor.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();

if (id != null)
{
InstructorID = id.Value;
}
}
}
}

El método OnGetAsync acepta datos de ruta opcionales para el identificador del instructor seleccionado.
Examine la consulta en la página Pages/Instructors/Index.cshtml:

Instructor.Instructors = await _context.Instructors


.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();

La consulta tiene dos instrucciones include:


OfficeAssignment : se muestra en la vista de instructores.
CourseAssignments : muestra los cursos impartidos.

Actualizar la página de índice de instructores


Actualice Pages/Instructors/Index.cshtml con el marcado siguiente:
@page "{id:int?}"
@model ContosoUniversity.Pages.Instructors.IndexModel

@{
ViewData["Title"] = "Instructors";
}

<h2>Instructors</h2>

<p>
<a asp-page="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>Last Name</th>
<th>First Name</th>
<th>Hire Date</th>
<th>Office</th>
<th>Courses</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Instructor.Instructors)
{
string selectedRow = "";
if (item.ID == Model.InstructorID)
{
selectedRow = "success";
}
<tr class="@selectedRow">
<td>
@Html.DisplayFor(modelItem => item.LastName)
</td>
<td>
@Html.DisplayFor(modelItem => item.FirstMidName)
</td>
<td>
@Html.DisplayFor(modelItem => item.HireDate)
</td>
<td>
@if (item.OfficeAssignment != null)
{
@item.OfficeAssignment.Location
}
</td>
<td>
@{
foreach (var course in item.CourseAssignments)
{
@course.Course.CourseID @: @course.Course.Title <br />
}
}
</td>
<td>
<a asp-page="./Index" asp-route-id="@item.ID">Select</a> |
<a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
En el marcado anterior se realizan los cambios siguientes:
Se actualiza la directiva page de @page a @page "{id:int?}" . "{id:int?}" es una plantilla de ruta. La
plantilla de ruta cambia las cadenas de consulta enteras de la dirección URL por datos de ruta. Por ejemplo,
al hacer clic en el vínculo Select de un instructor con únicamente la directiva @page , se genera una
dirección URL similar a la siguiente:
http://localhost:1234/Instructors?id=2

Cuando la directiva de página es @page "{id:int?}" , la dirección URL anterior es:


http://localhost:1234/Instructors/2

El título de página es Instructors.


Se ha agregado una columna Office en la que se muestra item.OfficeAssignment.Location solo si
item.OfficeAssignment no es NULL. Dado que se trata de una relación de uno a cero o uno, es posible que
no haya una entidad OfficeAssignment relacionada.

@if (item.OfficeAssignment != null)


{
@item.OfficeAssignment.Location
}

Se ha agregado una columna Courses en la que se muestran los cursos que imparte cada instructor. Vea
Transición de línea explícita con @: para obtener más información sobre esta sintaxis de Razor.
Ha agregado código que agrega dinámicamente class="success" al elemento tr del instructor
seleccionado. Esto establece el color de fondo de la fila seleccionada mediante una clase de arranque.

string selectedRow = "";


if (item.CourseID == Model.CourseID)
{
selectedRow = "success";
}
<tr class="@selectedRow">

Se ha agregado un hipervínculo nuevo con la etiqueta Select. Este vínculo envía el identificador del
instructor seleccionado al método Index y establece un color de fondo.

<a asp-action="Index" asp-route-id="@item.ID">Select</a> |

Ejecute la aplicación y haga clic en la pestaña Instructors. En la página se muestra la Location (oficina) de la
entidad OfficeAssignment relacionada. Si OfficeAssignment es NULL, se muestra una celda de tabla vacía.
Haga clic en el vínculo Select. El estilo de la fila cambia.
Agregar cursos impartidos por el instructor seleccionado
Actualice el método OnGetAsync de Pages/Instructors/Index.cshtml.cs con el código siguiente:

public async Task OnGetAsync(int? id, int? courseID)


{
Instructor = new InstructorIndexData();
Instructor.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();

if (id != null)
{
InstructorID = id.Value;
Instructor instructor = Instructor.Instructors.Where(
i => i.ID == id.Value).Single();
Instructor.Courses = instructor.CourseAssignments.Select(s => s.Course);
}

if (courseID != null)
{
CourseID = courseID.Value;
Instructor.Enrollments = Instructor.Courses.Where(
x => x.CourseID == courseID).Single().Enrollments;
}
}

Agregue public int CourseID { get; set; }


public class IndexModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;

public IndexModel(ContosoUniversity.Data.SchoolContext context)


{
_context = context;
}

public InstructorIndexData Instructor { get; set; }


public int InstructorID { get; set; }
public int CourseID { get; set; }

public async Task OnGetAsync(int? id, int? courseID)


{
Instructor = new InstructorIndexData();
Instructor.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();

if (id != null)
{
InstructorID = id.Value;
Instructor instructor = Instructor.Instructors.Where(
i => i.ID == id.Value).Single();
Instructor.Courses = instructor.CourseAssignments.Select(s => s.Course);
}

if (courseID != null)
{
CourseID = courseID.Value;
Instructor.Enrollments = Instructor.Courses.Where(
x => x.CourseID == courseID).Single().Enrollments;
}
}

Examine la consulta actualizada:

Instructor.Instructors = await _context.Instructors


.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();

En la consulta anterior se agregan las entidades Department .


El código siguiente se ejecuta cuando se selecciona un instructor ( id != null ). El instructor seleccionado se
recupera de la lista de instructores del modelo de vista. Se carga la propiedad Courses del modelo de vista con
las entidades Course de la propiedad de navegación CourseAssignments de ese instructor.
if (id != null)
{
InstructorID = id.Value;
Instructor instructor = Instructor.Instructors.Where(
i => i.ID == id.Value).Single();
Instructor.Courses = instructor.CourseAssignments.Select(s => s.Course);
}

El método Where devuelve una colección. En el método Where anterior, solo se devuelve una entidad Instructor
. El método Single convierte la colección en una sola entidad Instructor . La entidad Instructor proporciona
acceso a la propiedad CourseAssignments . CourseAssignments proporciona acceso a las entidades Course
relacionadas.

El método Single se usa en una colección cuando la colección tiene un solo elemento. El método Single inicia
una excepción si la colección está vacía o hay más de un elemento. Una alternativa es SingleOrDefault , que
devuelve una valor predeterminado (NULL, en este caso) si la colección está vacía. El uso de SingleOrDefault en
una colección vacía:
Inicia una excepción (al tratar de buscar una propiedad Courses en una referencia nula).
El mensaje de excepción indicará con menos claridad la causa del problema.
El código siguiente rellena la propiedad Enrollments del modelo de vista cuando se selecciona un curso:

if (courseID != null)
{
CourseID = courseID.Value;
Instructor.Enrollments = Instructor.Courses.Where(
x => x.CourseID == courseID).Single().Enrollments;
}

Agregue el siguiente marcado al final de la página de Razor Pages/Courses/Index.cshtml:


<a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>

@if (Model.Instructor.Courses != null)


{
<h3>Courses Taught by Selected Instructor</h3>
<table class="table">
<tr>
<th></th>
<th>Number</th>
<th>Title</th>
<th>Department</th>
</tr>

@foreach (var item in Model.Instructor.Courses)


{
string selectedRow = "";
if (item.CourseID == Model.CourseID)
{
selectedRow = "success";
}
<tr class="@selectedRow">
<td>
@Html.ActionLink("Select", "OnGetAsync",
new { courseID = item.CourseID })
</td>
<td>
@item.CourseID
</td>
<td>
@item.Title
</td> <td>
@item.Department.Name
</td>
</tr>
}

</table>
}

En el marcado anterior se muestra una lista de cursos relacionados con un instructor cuando se selecciona un
instructor.
Pruebe la aplicación. Haga clic en un vínculo Select en la página de instructores.
Mostrar datos de estudiante
En esta sección, la aplicación se actualiza para mostrar los datos de estudiante para un curso seleccionado.
Actualice la consulta en el método OnGetAsync de Pages/Instructors/Index.cshtml.cs con el código siguiente:

Instructor.Instructors = await _context.Instructors


.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Enrollments)
.ThenInclude(i => i.Student)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();

Actualice Pages/Instructors/Index.cshtml. Agregue el marcado siguiente al final del archivo:


@if (Model.Instructor.Enrollments != null)
{
<h3>
Students Enrolled in Selected Course
</h3>
<table class="table">
<tr>
<th>Name</th>
<th>Grade</th>
</tr>
@foreach (var item in Model.Instructor.Enrollments)
{
<tr>
<td>
@item.Student.FullName
</td>
<td>
@Html.DisplayFor(modelItem => item.Grade)
</td>
</tr>
}
</table>
}

En el marcado anterior se muestra una lista de los estudiantes que están inscritos en el curso seleccionado.
Actualice la página y seleccione un instructor. Seleccione un curso para ver la lista de los estudiantes inscritos y
sus calificaciones.
Uso de Single
Se puede pasar el método Single en la condición Where en lugar de llamar al método Where por separado:
public async Task OnGetAsync(int? id, int? courseID)
{
Instructor = new InstructorIndexData();

Instructor.Instructors = await _context.Instructors


.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Enrollments)
.ThenInclude(i => i.Student)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();

if (id != null)
{
InstructorID = id.Value;
Instructor instructor = Instructor.Instructors.Single(
i => i.ID == id.Value);
Instructor.Courses = instructor.CourseAssignments.Select(
s => s.Course);
}

if (courseID != null)
{
CourseID = courseID.Value;
Instructor.Enrollments = Instructor.Courses.Single(
x => x.CourseID == courseID).Enrollments;
}
}

El enfoque de Single anterior no ofrece ninguna ventaja con respecto a Where . Algunos desarrolladores
prefieren el estilo del enfoque de Single .

Carga explícita
En el código actual se especifica la carga diligente para Enrollments y Students :

Instructor.Instructors = await _context.Instructors


.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Enrollments)
.ThenInclude(i => i.Student)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();

Imagine que los usuarios rara vez querrán ver las inscripciones en un curso. En ese caso, una optimización sería
cargar solamente los datos de inscripción si se solicitan. En esta sección, se actualiza OnGetAsync para usar la
carga explícita de Enrollments y Students .
Actualice OnGetAsync con el código siguiente:
public async Task OnGetAsync(int? id, int? courseID)
{
Instructor = new InstructorIndexData();
Instructor.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
//.Include(i => i.CourseAssignments)
// .ThenInclude(i => i.Course)
// .ThenInclude(i => i.Enrollments)
// .ThenInclude(i => i.Student)
// .AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();

if (id != null)
{
InstructorID = id.Value;
Instructor instructor = Instructor.Instructors.Where(
i => i.ID == id.Value).Single();
Instructor.Courses = instructor.CourseAssignments.Select(s => s.Course);
}

if (courseID != null)
{
CourseID = courseID.Value;
var selectedCourse = Instructor.Courses.Where(x => x.CourseID == courseID).Single();
await _context.Entry(selectedCourse).Collection(x => x.Enrollments).LoadAsync();
foreach (Enrollment enrollment in selectedCourse.Enrollments)
{
await _context.Entry(enrollment).Reference(x => x.Student).LoadAsync();
}
Instructor.Enrollments = selectedCourse.Enrollments;
}
}

En el código anterior se quitan las llamadas al método ThenInclude para los datos de inscripción y estudiantes. Si
se selecciona un curso, el código resaltado recupera lo siguiente:
Las entidades Enrollment para el curso seleccionado.
Las entidades Student para cada Enrollment .

Tenga en cuenta que en el código anterior .AsNoTracking() se convierte en comentario. Las propiedades de
navegación solo se pueden cargar explícitamente para las entidades sometidas a seguimiento.
Pruebe la aplicación. Desde la perspectiva de los usuarios, la aplicación se comporta exactamente igual a la
versión anterior.
En el siguiente tutorial se muestra cómo actualizar datos relacionados.

A N T E R IO R S IG U IE N T E
Páginas de Razor con EF Core en ASP.NET Core:
Actualización de datos relacionados (7 de 8)
25/06/2018 • 24 minutes to read • Edit Online

Por Tom Dykstra y Rick Anderson


La aplicación web Contoso University muestra cómo crear aplicaciones web de las páginas de Razor con EF Core
y Visual Studio. Para obtener información sobre la serie de tutoriales, consulte el primer tutorial.
En este tutorial se muestra cómo actualizar datos relacionados. Si experimenta problemas que no puede resolver,
descargue la aplicación completada para esta fase.
En las ilustraciones siguientes se muestran algunas de las páginas completadas.
Examine y pruebe las páginas de cursos Create y Edit. Cree un curso. El departamento se selecciona por su clave
principal (un entero), no su nombre. Modifique el curso nuevo. Cuando haya terminado las pruebas, elimine el
curso nuevo.

Crear una clase base para compartir código común


En las páginas Courses/Create y Courses/Edit se necesita una lista de nombres de departamento. Cree la clase
base Pages/Courses/DepartmentNamePageModel.cshtml.cs para las páginas Create y Edit:
using ContosoUniversity.Data;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using System.Linq;

namespace ContosoUniversity.Pages.Courses
{
public class DepartmentNamePageModel : PageModel
{
public SelectList DepartmentNameSL { get; set; }

public void PopulateDepartmentsDropDownList(SchoolContext _context,


object selectedDepartment = null)
{
var departmentsQuery = from d in _context.Departments
orderby d.Name // Sort by name.
select d;

DepartmentNameSL = new SelectList(departmentsQuery.AsNoTracking(),


"DepartmentID", "Name", selectedDepartment);
}
}
}

En el código anterior se crea una clase SelectList para que contenga la lista de nombres de departamento. Si se
especifica selectedDepartment , se selecciona ese departamento en la SelectList .
Las clases de modelo de página de Create y Edit se derivan de DepartmentNamePageModel .

Personalizar las páginas de cursos


Cuando se crea una entidad de curso, debe tener una relación con un departamento existente. Para agregar un
departamento durante la creación de un curso, la clase base para Create y Edit contiene una lista desplegable para
seleccionar el departamento. La lista desplegable establece la propiedad de clave externa (FK)
Course.DepartmentID . EF Core usa la FK Course.DepartmentID para cargar la propiedad de navegación Department
.
Actualice el modelo de página de Create con el código siguiente:
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Courses
{
public class CreateModel : DepartmentNamePageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;

public CreateModel(ContosoUniversity.Data.SchoolContext context)


{
_context = context;
}

public IActionResult OnGet()


{
PopulateDepartmentsDropDownList(_context);
return Page();
}

[BindProperty]
public Course Course { get; set; }

public async Task<IActionResult> OnPostAsync()


{
if (!ModelState.IsValid)
{
return Page();
}

var emptyCourse = new Course();

if (await TryUpdateModelAsync<Course>(
emptyCourse,
"course", // Prefix for form value.
s => s.CourseID, s => s.DepartmentID, s => s.Title, s => s.Credits))
{
_context.Courses.Add(emptyCourse);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}

// Select DepartmentID if TryUpdateModelAsync fails.


PopulateDepartmentsDropDownList(_context, emptyCourse.DepartmentID);
return Page();
}
}
}

El código anterior:
Deriva de DepartmentNamePageModel .
Usa TryUpdateModelAsync para evitar la publicación excesiva.
Reemplaza ViewData["DepartmentID"] con DepartmentNameSL (de la clase base).

ViewData["DepartmentID"] se reemplaza con DepartmentNameSL fuertemente tipado. Los modelos fuertemente


tipados son preferibles a los de establecimiento flexible de tipos. Para obtener más información, vea
Establecimiento flexible de datos (ViewData y ViewBag).
Actualizar la página Courses Create
Actualice Pages/Courses/Create.cshtml con el marcado siguiente:
@page
@model ContosoUniversity.Pages.Courses.CreateModel
@{
ViewData["Title"] = "Create Course";
}
<h2>Create</h2>
<h4>Course</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Course.CourseID" class="control-label"></label>
<input asp-for="Course.CourseID" class="form-control" />
<span asp-validation-for="Course.CourseID" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Course.Title" class="control-label"></label>
<input asp-for="Course.Title" class="form-control" />
<span asp-validation-for="Course.Title" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Course.Credits" class="control-label"></label>
<input asp-for="Course.Credits" class="form-control" />
<span asp-validation-for="Course.Credits" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Course.Department" class="control-label"></label>
<select asp-for="Course.DepartmentID" class="form-control"
asp-items="@Model.DepartmentNameSL">
<option value="">-- Select Department --</option>
</select>
<span asp-validation-for="Course.DepartmentID" class="text-danger" />
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</form>
</div>
</div>
<div>
<a asp-page="Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

En el marcado anterior se realizan los cambios siguientes:


Se cambia el título de DepartmentID a Department.
Se reemplaza "ViewBag.DepartmentID" con DepartmentNameSL (de la clase base).
Se agrega la opción "Select Department" (Seleccionar departamento). Este cambio representa "Select
Department" en lugar del primer departamento.
Se agrega un mensaje de validación cuando el departamento no está seleccionado.
La página de Razor usa la Aplicación auxiliar de etiquetas de selección:
<div class="form-group">
<label asp-for="Course.Department" class="control-label"></label>
<select asp-for="Course.DepartmentID" class="form-control"
asp-items="@Model.DepartmentNameSL">
<option value="">-- Select Department --</option>
</select>
<span asp-validation-for="Course.DepartmentID" class="text-danger" />
</div>

Pruebe la página Create. En la página Create se muestra el nombre del departamento en lugar del identificador.
Actualice la página Courses Edit.
Actualice el modelo de página de Edit con el código siguiente:
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Courses
{
public class EditModel : DepartmentNamePageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;

public EditModel(ContosoUniversity.Data.SchoolContext context)


{
_context = context;
}

[BindProperty]
public Course Course { get; set; }

public async Task<IActionResult> OnGetAsync(int? id)


{
if (id == null)
{
return NotFound();
}

Course = await _context.Courses


.Include(c => c.Department).FirstOrDefaultAsync(m => m.CourseID == id);

if (Course == null)
{
return NotFound();
}

// Select current DepartmentID.


PopulateDepartmentsDropDownList(_context,Course.DepartmentID);
return Page();
}

public async Task<IActionResult> OnPostAsync(int? id)


{
if (!ModelState.IsValid)
{
return Page();
}

var courseToUpdate = await _context.Courses.FindAsync(id);

if (await TryUpdateModelAsync<Course>(
courseToUpdate,
"course", // Prefix for form value.
c => c.Credits, c => c.DepartmentID, c => c.Title))
{
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}

// Select DepartmentID if TryUpdateModelAsync fails.


PopulateDepartmentsDropDownList(_context, courseToUpdate.DepartmentID);
return Page();
}
}
}

Los cambios son similares a los realizados en el modelo de página de Create. En el código anterior,
PopulateDepartmentsDropDownList pasa el identificador de departamento, que selecciona el departamento
especificado en la lista desplegable.
Actualice Pages/Courses/Edit.cshtml con el marcado siguiente:

@page
@model ContosoUniversity.Pages.Courses.EditModel

@{
ViewData["Title"] = "Edit";
}

<h2>Edit</h2>

<h4>Course</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="Course.CourseID" />
<div class="form-group">
<label asp-for="Course.CourseID" class="control-label"></label>
<div>@Html.DisplayFor(model => model.Course.CourseID)</div>
</div>
<div class="form-group">
<label asp-for="Course.Title" class="control-label"></label>
<input asp-for="Course.Title" class="form-control" />
<span asp-validation-for="Course.Title" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Course.Credits" class="control-label"></label>
<input asp-for="Course.Credits" class="form-control" />
<span asp-validation-for="Course.Credits" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Course.Department" class="control-label"></label>
<select asp-for="Course.DepartmentID" class="form-control"
asp-items="@Model.DepartmentNameSL"></select>
<span asp-validation-for="Course.DepartmentID" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</form>
</div>
</div>

<div>
<a asp-page="./Index">Back to List</a>
</div>

@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

En el marcado anterior se realizan los cambios siguientes:


Se muestra el identificador del curso. Por lo general no se muestra la clave principal (PK) de una entidad. Las
PK normalmente no tienen sentido para los usuarios. En este caso, la clave principal es el número de curso.
Se cambia el título de DepartmentID a Department.
Se reemplaza "ViewBag.DepartmentID" con DepartmentNameSL (de la clase base).

La página contiene un campo oculto ( <input type="hidden"> ) para el número de curso. Agregar una aplicación
auxiliar de etiquetas <label> con asp-for="Course.CourseID" no elimina la necesidad del campo oculto. Se
requiere <input type="hidden"> para que el número de curso se incluya en los datos enviados cuando el usuario
hace clic en Guardar.
Pruebe el código actualizado. Cree, modifique y elimine un curso.

Agregar AsNoTracking a los modelos de página de Details y Delete


AsNoTracking puede mejorar el rendimiento cuando el seguimiento no es necesario. Agregue AsNoTracking al
modelo de página de Delete y Details. En el código siguiente se muestra el modelo de página de Delete
actualizado:

public class DeleteModel : PageModel


{
private readonly ContosoUniversity.Data.SchoolContext _context;

public DeleteModel(ContosoUniversity.Data.SchoolContext context)


{
_context = context;
}

[BindProperty]
public Course Course { get; set; }

public async Task<IActionResult> OnGetAsync(int? id)


{
if (id == null)
{
return NotFound();
}

Course = await _context.Courses


.AsNoTracking()
.Include(c => c.Department)
.FirstOrDefaultAsync(m => m.CourseID == id);

if (Course == null)
{
return NotFound();
}
return Page();
}

public async Task<IActionResult> OnPostAsync(int? id)


{
if (id == null)
{
return NotFound();
}

Course = await _context.Courses


.AsNoTracking()
.FirstOrDefaultAsync(m => m.CourseID == id);

if (Course != null)
{
_context.Courses.Remove(Course);
await _context.SaveChangesAsync();
}

return RedirectToPage("./Index");
}
}

Actualice el método OnGetAsync en el archivo Pages/Courses/Details.cshtml.cs:


public async Task<IActionResult> OnGetAsync(int? id)
{
if (id == null)
{
return NotFound();
}

Course = await _context.Courses


.AsNoTracking()
.Include(c => c.Department)
.FirstOrDefaultAsync(m => m.CourseID == id);

if (Course == null)
{
return NotFound();
}
return Page();
}

Modificar las páginas Delete y Details


Actualice la página de Razor Delete con el marcado siguiente:
@page
@model ContosoUniversity.Pages.Courses.DeleteModel

@{
ViewData["Title"] = "Delete";
}

<h2>Delete</h2>

<h3>Are you sure you want to delete this?</h3>


<div>
<h4>Course</h4>
<hr />
<dl class="dl-horizontal">
<dt>
@Html.DisplayNameFor(model => model.Course.CourseID)
</dt>
<dd>
@Html.DisplayFor(model => model.Course.CourseID)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Course.Title)
</dt>
<dd>
@Html.DisplayFor(model => model.Course.Title)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Course.Credits)
</dt>
<dd>
@Html.DisplayFor(model => model.Course.Credits)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Course.Department)
</dt>
<dd>
@Html.DisplayFor(model => model.Course.Department.DepartmentID)
</dd>
</dl>

<form method="post">
<input type="hidden" asp-for="Course.CourseID" />
<input type="submit" value="Delete" class="btn btn-default" /> |
<a asp-page="./Index">Back to List</a>
</form>
</div>

Realice los mismos cambios en la página Details.


Probar las páginas Course
Pruebe las páginas Create, Edit, Details y Delete.

Actualizar las páginas de instructor


En las siguientes secciones se actualizan las páginas de instructor.
Agregar la ubicación de la oficina
Al editar un registro de instructor, es posible que quiera actualizar la asignación de la oficina del instructor. La
entidad Instructor tiene una relación de uno a cero o uno con la entidad OfficeAssignment . El código de
instructor debe controlar lo siguiente:
Si el usuario desactiva la asignación de la oficina, elimine la entidad OfficeAssignment .
Si el usuario especifica una asignación de oficina y estaba vacía, cree una entidad OfficeAssignment .
Si el usuario cambia la asignación de oficina, actualice la entidad OfficeAssignment .
Actualice el modelo de página de Edit de los instructores con el código siguiente:

public class EditModel : PageModel


{
private readonly ContosoUniversity.Data.SchoolContext _context;

public EditModel(ContosoUniversity.Data.SchoolContext context)


{
_context = context;
}

[BindProperty]
public Instructor Instructor { get; set; }

public async Task<IActionResult> OnGetAsync(int? id)


{
if (id == null)
{
return NotFound();
}

Instructor = await _context.Instructors


.Include(i => i.OfficeAssignment)
.AsNoTracking()
.FirstOrDefaultAsync(m => m.ID == id);

if (Instructor == null)
{
return NotFound();
}
return Page();
}

public async Task<IActionResult> OnPostAsync(int? id)


{
if (!ModelState.IsValid)
{
return Page();
}

var instructorToUpdate = await _context.Instructors


.Include(i => i.OfficeAssignment)
.FirstOrDefaultAsync(s => s.ID == id);

if (await TryUpdateModelAsync<Instructor>(
instructorToUpdate,
"Instructor",
i => i.FirstMidName, i => i.LastName,
i => i.HireDate, i => i.OfficeAssignment))
{
if (String.IsNullOrWhiteSpace(
instructorToUpdate.OfficeAssignment?.Location))
{
instructorToUpdate.OfficeAssignment = null;
}
await _context.SaveChangesAsync();
}
return RedirectToPage("./Index");

}
}

El código anterior:
Obtiene la entidad Instructor actual de la base de datos mediante la carga diligente de la propiedad de
navegación OfficeAssignment .
Actualiza la entidad Instructor recuperada con valores del enlazador de modelos. TryUpdateModel evita la
publicación excesiva.
Si la ubicación de la oficina está en blanco, establece Instructor.OfficeAssignment en NULL. Cuando
Instructor.OfficeAssignment es NULL, se elimina la fila relacionada en la tabla OfficeAssignment .

Actualizar la página Edit del instructor


Actualice Pages/Instructors/Edit.cshtml con la ubicación de la oficina:

@page
@model ContosoUniversity.Pages.Instructors.EditModel
@{
ViewData["Title"] = "Edit";
}
<h2>Edit</h2>
<h4>Instructor</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="Instructor.ID" />
<div class="form-group">
<label asp-for="Instructor.LastName" class="control-label"></label>
<input asp-for="Instructor.LastName" class="form-control" />
<span asp-validation-for="Instructor.LastName" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Instructor.FirstMidName" class="control-label"></label>
<input asp-for="Instructor.FirstMidName" class="form-control" />
<span asp-validation-for="Instructor.FirstMidName" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Instructor.HireDate" class="control-label"></label>
<input asp-for="Instructor.HireDate" class="form-control" />
<span asp-validation-for="Instructor.HireDate" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Instructor.OfficeAssignment.Location" class="control-label"></label>
<input asp-for="Instructor.OfficeAssignment.Location" class="form-control" />
<span asp-validation-for="Instructor.OfficeAssignment.Location" class="text-danger" />
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</form>
</div>
</div>

<div>
<a asp-page="./Index">Back to List</a>
</div>

@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

Compruebe que puede cambiar la ubicación de la oficina de un instructor.

Agregar asignaciones de cursos a la página Edit de los instructores


Los instructores pueden impartir cualquier número de cursos. En esta sección, agregará la capacidad de cambiar
las asignaciones de cursos. En la imagen siguiente se muestra la página Edit actualizada de los instructores:

Course e Instructor tienen una relación de varios a varios. Para agregar y eliminar relaciones, agregue y quite
entidades del conjunto de entidades combinadas CourseAssignments .
Las casillas permiten cambios en los cursos a los que está asignado un instructor. Se muestra una casilla para
cada curso en la base de datos. Los cursos a los que el instructor está asignado se activan. El usuario puede activar
o desactivar las casillas para cambiar las asignaciones de cursos. Si el número de cursos fuera mucho mayor:
Probablemente usaría una interfaz de usuario diferente para mostrar los cursos.
El método de manipulación de una entidad de combinación para crear o eliminar relaciones no cambiaría.
Agregar clases para admitir las páginas de instructor Create y Edit
Cree SchoolViewModels/AssignedCourseData.cs con el código siguiente:

namespace ContosoUniversity.Models.SchoolViewModels
{
public class AssignedCourseData
{
public int CourseID { get; set; }
public string Title { get; set; }
public bool Assigned { get; set; }
}
}

La clase AssignedCourseData contiene datos para crear las casillas para los cursos asignados por un instructor.
Cree la clase base Pages/Instructors/InstructorCoursesPageModel.cshtml.cs:

using ContosoUniversity.Data;
using ContosoUniversity.Models;
using ContosoUniversity.Models.SchoolViewModels;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.Collections.Generic;
using System.Linq;

namespace ContosoUniversity.Pages.Instructors
{
public class InstructorCoursesPageModel : PageModel
{

public List<AssignedCourseData> AssignedCourseDataList;

public void PopulateAssignedCourseData(SchoolContext context,


Instructor instructor)
{
var allCourses = context.Courses;
var instructorCourses = new HashSet<int>(
instructor.CourseAssignments.Select(c => c.CourseID));
AssignedCourseDataList = new List<AssignedCourseData>();
foreach (var course in allCourses)
{
AssignedCourseDataList.Add(new AssignedCourseData
{
CourseID = course.CourseID,
Title = course.Title,
Assigned = instructorCourses.Contains(course.CourseID)
});
}
}

public void UpdateInstructorCourses(SchoolContext context,


string[] selectedCourses, Instructor instructorToUpdate)
{
if (selectedCourses == null)
{
instructorToUpdate.CourseAssignments = new List<CourseAssignment>();
return;
}

var selectedCoursesHS = new HashSet<string>(selectedCourses);


var instructorCourses = new HashSet<int>
(instructorToUpdate.CourseAssignments.Select(c => c.Course.CourseID));
foreach (var course in context.Courses)
{
if (selectedCoursesHS.Contains(course.CourseID.ToString()))
{
if (!instructorCourses.Contains(course.CourseID))
{
instructorToUpdate.CourseAssignments.Add(
new CourseAssignment
{
InstructorID = instructorToUpdate.ID,
CourseID = course.CourseID
});
}
}
else
{
if (instructorCourses.Contains(course.CourseID))
{
CourseAssignment courseToRemove
= instructorToUpdate
.CourseAssignments
.SingleOrDefault(i => i.CourseID == course.CourseID);
.SingleOrDefault(i => i.CourseID == course.CourseID);
context.Remove(courseToRemove);
}
}
}
}
}
}

InstructorCoursesPageModel es la clase base que se usará para los modelos de página de Edit y Create.
PopulateAssignedCourseData lee todas las entidades Course para rellenar AssignedCourseDataList . Para cada
curso, el código establece el CourseID , el título y si el instructor está asignado o no al curso. Se usa un HashSet
para crear búsquedas eficaces.
Modelo de página de edición de instructores
Actualice el modelo de página de edición de instructores con el código siguiente:
public class EditModel : InstructorCoursesPageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;

public EditModel(ContosoUniversity.Data.SchoolContext context)


{
_context = context;
}

[BindProperty]
public Instructor Instructor { get; set; }

public async Task<IActionResult> OnGetAsync(int? id)


{
if (id == null)
{
return NotFound();
}

Instructor = await _context.Instructors


.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments).ThenInclude(i => i.Course)
.AsNoTracking()
.FirstOrDefaultAsync(m => m.ID == id);

if (Instructor == null)
{
return NotFound();
}
PopulateAssignedCourseData(_context, Instructor);
return Page();
}

public async Task<IActionResult> OnPostAsync(int? id, string[] selectedCourses)


{
if (!ModelState.IsValid)
{
return Page();
}

var instructorToUpdate = await _context.Instructors


.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.FirstOrDefaultAsync(s => s.ID == id);

if (await TryUpdateModelAsync<Instructor>(
instructorToUpdate,
"Instructor",
i => i.FirstMidName, i => i.LastName,
i => i.HireDate, i => i.OfficeAssignment))
{
if (String.IsNullOrWhiteSpace(
instructorToUpdate.OfficeAssignment?.Location))
{
instructorToUpdate.OfficeAssignment = null;
}
UpdateInstructorCourses(_context, selectedCourses, instructorToUpdate);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
UpdateInstructorCourses(_context, selectedCourses, instructorToUpdate);
PopulateAssignedCourseData(_context, instructorToUpdate);
return Page();
}
}
El código anterior controla los cambios de asignación de oficina.
Actualice la vista de Razor del instructor:

@page
@model ContosoUniversity.Pages.Instructors.EditModel
@{
ViewData["Title"] = "Edit";
}
<h2>Edit</h2>
<h4>Instructor</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="Instructor.ID" />
<div class="form-group">
<label asp-for="Instructor.LastName" class="control-label"></label>
<input asp-for="Instructor.LastName" class="form-control" />
<span asp-validation-for="Instructor.LastName" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Instructor.FirstMidName" class="control-label"></label>
<input asp-for="Instructor.FirstMidName" class="form-control" />
<span asp-validation-for="Instructor.FirstMidName" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Instructor.HireDate" class="control-label"></label>
<input asp-for="Instructor.HireDate" class="form-control" />
<span asp-validation-for="Instructor.HireDate" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Instructor.OfficeAssignment.Location" class="control-label"></label>
<input asp-for="Instructor.OfficeAssignment.Location" class="form-control" />
<span asp-validation-for="Instructor.OfficeAssignment.Location" class="text-danger" />
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<table>
<tr>
@{
int cnt = 0;

foreach (var course in Model.AssignedCourseDataList)


{
if (cnt++ % 3 == 0)
{
@:</tr><tr>
}
@:<td>
<input type="checkbox"
name="selectedCourses"
value="@course.CourseID"
@(Html.Raw(course.Assigned ? "checked=\"checked\"" : "")) />
@course.CourseID @: @course.Title
@:</td>
}
@:</tr>
}
</table>
</div>
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</form>
</div>
</div>
</div>

<div>
<a asp-page="./Index">Back to List</a>
</div>

@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

NOTE
Al pegar el código en Visual Studio, se cambian los saltos de línea de tal forma que el código se interrumpe. Presione Ctrl+Z
una vez para deshacer el formato automático. Ctrl+Z corrige los saltos de línea para que se muestren como se ven aquí. No
es necesario que la sangría sea perfecta, pero las líneas @</tr><tr> , @:<td> , @:</td> y @:</tr> deben estar en una
única línea tal y como se muestra. Con el bloque de código nuevo seleccionado, presione tres veces la tecla Tab para
alinearlo con el código existente. Puede votar o revisar el estado de este error con este vínculo.

En el código anterior se crea una tabla HTML que tiene tres columnas. Cada columna tiene una casilla y una
leyenda que contiene el número y el título del curso. Todas las casillas tienen el mismo nombre
("selectedCourses"). Al usar el mismo nombre se informa al enlazador de modelos que las trate como un grupo. El
atributo de valor de cada casilla se establece en CourseID . Cuando se envía la página, el enlazador de modelos
pasa una matriz formada solo por los valores CourseID de las casillas activadas.
Cuando se representan las casillas por primera vez, los cursos asignados al instructor tienen atributos checked.
Ejecute la aplicación y pruebe la página Edit de los instructores actualizada. Cambie algunas asignaciones de
cursos. Los cambios se reflejan en la página Index.
Nota: El enfoque que se aplica aquí para modificar datos de los cursos del instructor funciona bien cuando hay un
número limitado de cursos. Para las colecciones que son mucho más grandes, una interfaz de usuario y un
método de actualización diferentes serían más eficaces y útiles.
Actualizar la página de creación de instructores
Actualice el modelo de página de creación de instructores con el código siguiente:

using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Instructors
{
public class CreateModel : InstructorCoursesPageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;

public CreateModel(ContosoUniversity.Data.SchoolContext context)


{
_context = context;
}

public IActionResult OnGet()


{
var instructor = new Instructor();
instructor.CourseAssignments = new List<CourseAssignment>();

// Provides an empty collection for the foreach loop


// foreach (var course in Model.AssignedCourseDataList)
// in the Create Razor page.
PopulateAssignedCourseData(_context, instructor);
PopulateAssignedCourseData(_context, instructor);
return Page();
}

[BindProperty]
public Instructor Instructor { get; set; }

public async Task<IActionResult> OnPostAsync(string[] selectedCourses)


{
if (!ModelState.IsValid)
{
return Page();
}

var newInstructor = new Instructor();


if (selectedCourses != null)
{
newInstructor.CourseAssignments = new List<CourseAssignment>();
foreach (var course in selectedCourses)
{
var courseToAdd = new CourseAssignment
{
CourseID = int.Parse(course)
};
newInstructor.CourseAssignments.Add(courseToAdd);
}
}

if (await TryUpdateModelAsync<Instructor>(
newInstructor,
"Instructor",
i => i.FirstMidName, i => i.LastName,
i => i.HireDate, i => i.OfficeAssignment))
{
_context.Instructors.Add(newInstructor);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
PopulateAssignedCourseData(_context, newInstructor);
return Page();
}
}
}

El código anterior es similar al de Pages/Instructors/Edit.cshtml.cs.


Actualice la página de Razor de creación de instructores con el marcado siguiente:

@page
@model ContosoUniversity.Pages.Instructors.CreateModel

@{
ViewData["Title"] = "Create";
}

<h2>Create</h2>

<h4>Instructor</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Instructor.LastName" class="control-label"></label>
<input asp-for="Instructor.LastName" class="form-control" />
<span asp-validation-for="Instructor.LastName" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="Instructor.FirstMidName" class="control-label"></label>
<input asp-for="Instructor.FirstMidName" class="form-control" />
<span asp-validation-for="Instructor.FirstMidName" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Instructor.HireDate" class="control-label"></label>
<input asp-for="Instructor.HireDate" class="form-control" />
<span asp-validation-for="Instructor.HireDate" class="text-danger"></span>
</div>

<div class="form-group">
<label asp-for="Instructor.OfficeAssignment.Location" class="control-label"></label>
<input asp-for="Instructor.OfficeAssignment.Location" class="form-control" />
<span asp-validation-for="Instructor.OfficeAssignment.Location" class="text-danger" />
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<table>
<tr>
@{
int cnt = 0;

foreach (var course in Model.AssignedCourseDataList)


{
if (cnt++ % 3 == 0)
{
@:</tr><tr>
}
@:<td>
<input type="checkbox"
name="selectedCourses"
value="@course.CourseID"
@(Html.Raw(course.Assigned ? "checked=\"checked\"" : "")) />
@course.CourseID @: @course.Title
@:</td>
}
@:</tr>
}
</table>
</div>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</form>
</div>
</div>

<div>
<a asp-page="Index">Back to List</a>
</div>

@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

Pruebe la página de creación de instructores.

Actualizar la página Delete


Actualice el modelo de la página Delete con el código siguiente:
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Instructors
{
public class DeleteModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;

public DeleteModel(ContosoUniversity.Data.SchoolContext context)


{
_context = context;
}

[BindProperty]
public Instructor Instructor { get; set; }

public async Task<IActionResult> OnGetAsync(int? id)


{
if (id == null)
{
return NotFound();
}

Instructor = await _context.Instructors.SingleAsync(m => m.ID == id);

if (Instructor == null)
{
return NotFound();
}
return Page();
}

public async Task<IActionResult> OnPostAsync(int id)


{
Instructor instructor = await _context.Instructors
.Include(i => i.CourseAssignments)
.SingleAsync(i => i.ID == id);

var departments = await _context.Departments


.Where(d => d.InstructorID == id)
.ToListAsync();
departments.ForEach(d => d.InstructorID = null);

_context.Instructors.Remove(instructor);

await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
}
}

En el código anterior se realizan los cambios siguientes:


Se usa la carga diligente para la propiedad de navegación CourseAssignments . Es necesario incluir
CourseAssignments o no se eliminarán cuando se elimine el instructor. Para evitar la necesidad de leerlos,
configure la eliminación en cascada en la base de datos.
Si el instructor que se va a eliminar está asignado como administrador de cualquiera de los
departamentos, quita la asignación de instructor de esos departamentos.
A N T E R IO R S IG U IE N T E
25/06/2018 • 27 minutes to read • Edit Online

es-es/

Páginas de Razor con EF Core en ASP.NET Core:


Simultaneidad (8 de 8)
Por Rick Anderson, Tom Dykstra y Jon P Smith
La aplicación web Contoso University muestra cómo crear aplicaciones web de las páginas de Razor con EF
Core y Visual Studio. Para obtener información sobre la serie de tutoriales, consulte el primer tutorial.
Este tutorial muestra cómo tratar los conflictos cuando varios usuarios actualizan una entidad de forma
simultánea (al mismo tiempo). Si experimenta problemas que no puede resolver, descargue la aplicación
completada para esta fase.

Conflictos de simultaneidad
Un conflicto de simultaneidad se produce cuando:
Un usuario va a la página de edición de una entidad.
Otro usuario actualiza la misma entidad antes de que el cambio del primer usuario se escriba en la base de
datos.
Si no está habilitada la detección de simultaneidad, cuando se produzcan actualizaciones simultáneas:
Prevalece la última actualización. Es decir, los últimos valores de actualización se guardan en la base de datos.
La primera de las actualizaciones actuales se pierde.
Simultaneidad optimista
La simultaneidad optimista permite que se produzcan conflictos de simultaneidad y luego reacciona
correctamente si ocurren. Por ejemplo, Jane visita la página de edición de Department y cambia el presupuesto
para el departamento de inglés de 350.000,00 a 0,00 USD.
Antes de que Jane haga clic en Save, John visita la misma página y cambia el campo Start Date de 9/1/2007 a
9/1/2013.
Jane hace clic en Save primero y ve su cambio cuando el explorador muestra la página de índice.

John hace clic en Save en una página Edit que sigue mostrando un presupuesto de 350.000,00 USD. Lo que
sucede después viene determinado por cómo controla los conflictos de simultaneidad.
La simultaneidad optimista incluye las siguientes opciones:
Puede realizar un seguimiento de la propiedad que ha modificado un usuario y actualizar solo las
columnas correspondientes de la base de datos.
En el escenario, no se perderá ningún dato. Los dos usuarios actualizaron diferentes propiedades. La
próxima vez que un usuario examine el departamento de inglés, verá los cambios tanto de Jane como de
John. Este método de actualización puede reducir el número de conflictos que pueden dar lugar a una
pérdida de datos. Este enfoque: * No puede evitar la pérdida de datos si se realizan cambios paralelos a la
misma propiedad. * Por lo general no es práctico en una aplicación web. Requiere mantener un estado
significativo para realizar un seguimiento de todos los valores capturados y nuevos. El mantenimiento de
grandes cantidades de estado puede afectar al rendimiento de la aplicación. * Puede aumentar la
complejidad de las aplicaciones en comparación con la detección de simultaneidad en una entidad.
Puede permitir que los cambios de John sobrescriban los cambios de Jane.
La próxima vez que un usuario examine el departamento de inglés, verá 9/1/2013 y el valor de
350.000,00 USD capturado. Este enfoque se denomina un escenario de Prevalece el cliente o Prevalece el
último. (Todos los valores del cliente tienen prioridad sobre lo que aparece en el almacén de datos). Si no
hace ninguna codificación para el control de la simultaneidad, Prevalece el cliente se realizará
automáticamente.
Puede evitar que el cambio de John se actualice en la base de datos. Normalmente, la aplicación podría: *
Mostrar un mensaje de error. * Mostrar el estado actual de los datos. * Permitir al usuario volver a aplicar
los cambios.
Esto se denomina un escenario de Prevalece el almacén. (Los valores del almacén de datos tienen
prioridad sobre los valores enviados por el cliente). En este tutorial implementará el escenario de
Prevalece el almacén. Este método garantiza que ningún cambio se sobrescriba sin que se avise al
usuario.

Administrar la simultaneidad
Cuando una propiedad se configura como un token de simultaneidad:
EF Core comprueba que no se ha modificado la propiedad después de que se capturase. La comprobación se
produce cuando se llama a SaveChanges o SaveChangesAsync.
Si se ha cambiado la propiedad después de haberla capturado, se produce una excepción
DbUpdateConcurrencyException.
Deben configurarse el modelo de datos y la base de datos para que admitan producir una excepción
DbUpdateConcurrencyException .

Detectar conflictos de simultaneidad en una propiedad


Se pueden detectar conflictos de simultaneidad en el nivel de propiedad con el atributo ConcurrencyCheck. El
atributo se puede aplicar a varias propiedades en el modelo. Para obtener más información, consulte
Anotaciones de datos: ConcurrencyCheck.
El atributo [ConcurrencyCheck] no se usa en este tutorial.
Detectar conflictos de simultaneidad en una fila
Para detectar conflictos de simultaneidad, se agrega al modelo una columna de seguimiento rowversion.
rowversion :

Es específico de SQL Server. Otras bases de datos podrían no proporcionar una característica similar.
Se usa para determinar que no se ha cambiado una entidad desde que se capturó de la base de datos.
La base de datos genera un número rowversion secuencial que se incrementa cada vez que se actualiza la fila.
En un comando Update o Delete , la cláusula Where incluye el valor capturado de rowversion . Si la fila que se
está actualizando ha cambiado:
rowversionno coincide con el valor capturado.
Los comandos Update o Delete no encuentran una fila porque la cláusula Where incluye la rowversion
capturada.
Se produce una excepción DbUpdateConcurrencyException .
En EF Core, cuando un comando Update o Delete no han actualizado ninguna fila, se produce una excepción
de simultaneidad.
Agregar una propiedad de seguimiento a la entidad Department
En Models/Department.cs, agregue una propiedad de seguimiento denominada RowVersion:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
public class Department
{
public int DepartmentID { get; set; }

[StringLength(50, MinimumLength = 3)]


public string Name { get; set; }

[DataType(DataType.Currency)]
[Column(TypeName = "money")]
public decimal Budget { get; set; }

[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
[Display(Name = "Start Date")]
public DateTime StartDate { get; set; }

public int? InstructorID { get; set; }

[Timestamp]
public byte[] RowVersion { get; set; }

public Instructor Administrator { get; set; }


public ICollection<Course> Courses { get; set; }
}
}

El atributo Timestamp especifica que esta columna se incluye en la cláusula Where de los comandos Update y
Delete . El atributo se denomina Timestamp porque las versiones anteriores de SQL Server usaban un tipo de
datos timestamp antes de que el tipo rowversion de SQL lo sustituyera por otro.
La API fluida también puede especificar la propiedad de seguimiento:

modelBuilder.Entity<Department>()
.Property<byte[]>("RowVersion")
.IsRowVersion();

El código siguiente muestra una parte del T-SQL generado por EF Core cuando se actualiza el nombre de
Department:

SET NOCOUNT ON;


UPDATE [Department] SET [Name] = @p0
WHERE [DepartmentID] = @p1 AND [RowVersion] = @p2;
SELECT [RowVersion]
FROM [Department]
WHERE @@ROWCOUNT = 1 AND [DepartmentID] = @p1;

El código resaltado anteriormente muestra la cláusula WHERE que contiene RowVersion . Si la base de datos
RowVersion no es igual al parámetro RowVersion ( @p2 ), no se ha actualizado ninguna fila.
El código resaltado a continuación muestra el T-SQL que comprueba que se actualizó exactamente una fila:

SET NOCOUNT ON;


UPDATE [Department] SET [Name] = @p0
WHERE [DepartmentID] = @p1 AND [RowVersion] = @p2;
SELECT [RowVersion]
FROM [Department]
WHERE @@ROWCOUNT = 1 AND [DepartmentID] = @p1;

@@ROWCOUNT devuelve el número de filas afectadas por la última instrucción. Si no se actualiza ninguna fila,
EF Core produce una excepción DbUpdateConcurrencyException .
Puede ver el T-SQL que genera EF Core en la ventana de salida de Visual Studio.
Actualizar la base de datos
Agregar la propiedad RowVersion cambia el modelo de base de datos, lo que requiere una migración.
Compile el proyecto. Escriba lo siguiente en una ventana de comandos:

dotnet ef migrations add RowVersion


dotnet ef database update

Los comandos anteriores:


Agregan el archivo de migración Migrations/{time stamp }_RowVersion.cs.
Actualizan el archivo Migrations/SchoolContextModelSnapshot.cs. La actualización agrega el siguiente código
resaltado al método BuildModel :

modelBuilder.Entity("ContosoUniversity.Models.Department", b =>
{
b.Property<int>("DepartmentID")
.ValueGeneratedOnAdd();

b.Property<decimal>("Budget")
.HasColumnType("money");

b.Property<int?>("InstructorID");

b.Property<string>("Name")
.HasMaxLength(50);

b.Property<byte[]>("RowVersion")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate();

b.Property<DateTime>("StartDate");

b.HasKey("DepartmentID");

b.HasIndex("InstructorID");

b.ToTable("Department");
});

Ejecutan las migraciones para actualizar la base de datos.

Aplicar la técnica scaffolding al modelo Departments


Salga de Visual Studio.
Abra una ventana de comandos en el directorio del proyecto (el directorio que contiene los archivos
Program.cs, Startup.cs y .csproj).
Ejecute el siguiente comando:

dotnet aspnet-codegenerator razorpage -m Department -dc SchoolContext -udl -outDir Pages\Departments -


-referenceScriptLibraries

El comando anterior aplica scaffolding al modelo Department . Abra el proyecto en Visual Studio.
Compile el proyecto. La compilación genera errores similares a los siguientes:
1>Pages/Departments/Index.cshtml.cs(26,37,26,43): error CS1061: 'SchoolContext' does not contain a definition
for 'Department' and no extension method 'Department' accepting a first argument of type 'SchoolContext'
could be found (are you missing a using directive or an assembly reference?)

Cambie globalmente _context.Department por _context.Departments (es decir, agregue una "s" a Department ).
Se encuentran y actualizan siete repeticiones.
Actualizar la página de índice de Departments
El motor de scaffolding creó una columna RowVersion para la página de índice, pero ese campo no debería
mostrarse. En este tutorial, el último byte de la RowVersion se muestra para ayudar a entender la simultaneidad.
No se garantiza que el último byte sea único. Una aplicación real no mostraría RowVersion ni el último byte de
RowVersion .

Actualice la página Index:


Reemplace Index por Departments.
Reemplace el marcado que contiene RowVersion por el último byte de RowVersion .
Reemplace FirstMidName por FullName.
El marcado siguiente muestra la página actualizada:
@page
@model ContosoUniversity.Pages.Departments.IndexModel

@{
ViewData["Title"] = "Departments";
}

<h2>Departments</h2>

<p>
<a asp-page="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Department[0].Name)
</th>
<th>
@Html.DisplayNameFor(model => model.Department[0].Budget)
</th>
<th>
@Html.DisplayNameFor(model => model.Department[0].StartDate)
</th>
<th>
@Html.DisplayNameFor(model => model.Department[0].Administrator)
</th>
<th>
RowVersion
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Department) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.Name)
</td>
<td>
@Html.DisplayFor(modelItem => item.Budget)
</td>
<td>
@Html.DisplayFor(modelItem => item.StartDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Administrator.FullName)
</td>
<td>
@item.RowVersion[7]
</td>
<td>
<a asp-page="./Edit" asp-route-id="@item.DepartmentID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.DepartmentID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.DepartmentID">Delete</a>
</td>
</tr>
}
</tbody>
</table>

Actualizar el modelo de la página Edit


Actualice pages\departments\edit.cshtml.cs con el código siguiente:

using ContosoUniversity.Data;
using ContosoUniversity.Models;
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Departments
{
public class EditModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;

public EditModel(ContosoUniversity.Data.SchoolContext context)


{
_context = context;
}

[BindProperty]
public Department Department { get; set; }
// Replace ViewData["InstructorID"]
public SelectList InstructorNameSL { get; set; }

public async Task<IActionResult> OnGetAsync(int id)


{
Department = await _context.Departments
.Include(d => d.Administrator) // eager loading
.AsNoTracking() // tracking not required
.FirstOrDefaultAsync(m => m.DepartmentID == id);

if (Department == null)
{
return NotFound();
}

// Use strongly typed data rather than ViewData.


InstructorNameSL = new SelectList(_context.Instructors,
"ID", "FirstMidName");

return Page();
}

public async Task<IActionResult> OnPostAsync(int id)


{
if (!ModelState.IsValid)
{
return Page();
}

var departmentToUpdate = await _context.Departments


.Include(i => i.Administrator)
.FirstOrDefaultAsync(m => m.DepartmentID == id);

// null means Department was deleted by another user.


if (departmentToUpdate == null)
{
return await HandleDeletedDepartment();
}

// Update the RowVersion to the value when this entity was


// fetched. If the entity has been updated after it was
// fetched, RowVersion won't match the DB RowVersion and
// a DbUpdateConcurrencyException is thrown.
// A second postback will make them match, unless a new
// concurrency issue happens.
_context.Entry(departmentToUpdate)
.Property("RowVersion").OriginalValue = Department.RowVersion;

if (await TryUpdateModelAsync<Department>(
if (await TryUpdateModelAsync<Department>(
departmentToUpdate,
"Department",
s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
{
try
{
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
catch (DbUpdateConcurrencyException ex)
{
var exceptionEntry = ex.Entries.Single();
var clientValues = (Department)exceptionEntry.Entity;
var databaseEntry = exceptionEntry.GetDatabaseValues();
if (databaseEntry == null)
{
ModelState.AddModelError(string.Empty, "Unable to save. " +
"The department was deleted by another user.");
return Page();
}

var dbValues = (Department)databaseEntry.ToObject();


await setDbErrorMessage(dbValues, clientValues, _context);

// Save the current RowVersion so next postback


// matches unless an new concurrency issue happens.
Department.RowVersion = (byte[])dbValues.RowVersion;
// Must clear the model error for the next postback.
ModelState.Remove("Department.RowVersion");
}
}

InstructorNameSL = new SelectList(_context.Instructors,


"ID", "FullName", departmentToUpdate.InstructorID);

return Page();
}

private async Task<IActionResult> HandleDeletedDepartment()


{
Department deletedDepartment = new Department();
// ModelState contains the posted data because of the deletion error and will overide the
Department instance values when displaying Page().
ModelState.AddModelError(string.Empty,
"Unable to save. The department was deleted by another user.");
InstructorNameSL = new SelectList(_context.Instructors, "ID", "FullName",
Department.InstructorID);
return Page();
}

private async Task setDbErrorMessage(Department dbValues,


Department clientValues, SchoolContext context)
{

if (dbValues.Name != clientValues.Name)
{
ModelState.AddModelError("Department.Name",
$"Current value: {dbValues.Name}");
}
if (dbValues.Budget != clientValues.Budget)
{
ModelState.AddModelError("Department.Budget",
$"Current value: {dbValues.Budget:c}");
}
if (dbValues.StartDate != clientValues.StartDate)
{
ModelState.AddModelError("Department.StartDate",
$"Current value: {dbValues.StartDate:d}");
}
if (dbValues.InstructorID != clientValues.InstructorID)
{
Instructor dbInstructor = await _context.Instructors
.FindAsync(dbValues.InstructorID);
ModelState.AddModelError("Department.InstructorID",
$"Current value: {dbInstructor?.FullName}");
}

ModelState.AddModelError(string.Empty,
"The record you attempted to edit "
+ "was modified by another user after you. The "
+ "edit operation was canceled and the current values in the database "
+ "have been displayed. If you still want to edit this record, click "
+ "the Save button again.");
}
}
}

Para detectar un problema de simultaneidad, el OriginalValue se actualiza con el valor rowVersion de la entidad
de la que se capturó. EF Core genera un comando UPDATE de SQL con una cláusula WHERE que contiene el
valor RowVersion original. Si no hay ninguna fila afectada por el comando UPDATE (ninguna fila tiene el valor
RowVersion original), se produce una excepción DbUpdateConcurrencyException .

public async Task<IActionResult> OnPostAsync(int id)


{
if (!ModelState.IsValid)
{
return Page();
}

var departmentToUpdate = await _context.Departments


.Include(i => i.Administrator)
.FirstOrDefaultAsync(m => m.DepartmentID == id);

// null means Department was deleted by another user.


if (departmentToUpdate == null)
{
return await HandleDeletedDepartment();
}

// Update the RowVersion to the value when this entity was


// fetched. If the entity has been updated after it was
// fetched, RowVersion won't match the DB RowVersion and
// a DbUpdateConcurrencyException is thrown.
// A second postback will make them match, unless a new
// concurrency issue happens.
_context.Entry(departmentToUpdate)
.Property("RowVersion").OriginalValue = Department.RowVersion;

En el código anterior, Department.RowVersion es el valor cuando se capturó la entidad. OriginalValue es el valor


de la base de datos cuando se llamó a FirstOrDefaultAsync en este método.
El código siguiente obtiene los valores de cliente (es decir, los valores registrados en este método) y los valores
de la base de datos:
try
{
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
catch (DbUpdateConcurrencyException ex)
{
var exceptionEntry = ex.Entries.Single();
var clientValues = (Department)exceptionEntry.Entity;
var databaseEntry = exceptionEntry.GetDatabaseValues();
if (databaseEntry == null)
{
ModelState.AddModelError(string.Empty, "Unable to save. " +
"The department was deleted by another user.");
return Page();
}

var dbValues = (Department)databaseEntry.ToObject();


await setDbErrorMessage(dbValues, clientValues, _context);

// Save the current RowVersion so next postback


// matches unless an new concurrency issue happens.
Department.RowVersion = (byte[])dbValues.RowVersion;
// Must clear the model error for the next postback.
ModelState.Remove("Department.RowVersion");
}

El código siguiente agrega un mensaje de error personalizado para cada columna que tiene valores de la base de
datos diferentes de lo que se registró en OnPostAsync :

private async Task setDbErrorMessage(Department dbValues,


Department clientValues, SchoolContext context)
{

if (dbValues.Name != clientValues.Name)
{
ModelState.AddModelError("Department.Name",
$"Current value: {dbValues.Name}");
}
if (dbValues.Budget != clientValues.Budget)
{
ModelState.AddModelError("Department.Budget",
$"Current value: {dbValues.Budget:c}");
}
if (dbValues.StartDate != clientValues.StartDate)
{
ModelState.AddModelError("Department.StartDate",
$"Current value: {dbValues.StartDate:d}");
}
if (dbValues.InstructorID != clientValues.InstructorID)
{
Instructor dbInstructor = await _context.Instructors
.FindAsync(dbValues.InstructorID);
ModelState.AddModelError("Department.InstructorID",
$"Current value: {dbInstructor?.FullName}");
}

ModelState.AddModelError(string.Empty,
"The record you attempted to edit "
+ "was modified by another user after you. The "
+ "edit operation was canceled and the current values in the database "
+ "have been displayed. If you still want to edit this record, click "
+ "the Save button again.");
}
El código resaltado a continuación establece el valor RowVersion para el nuevo valor recuperado de la base de
datos. La próxima vez que el usuario haga clic en Save, solo se detectarán los errores de simultaneidad que se
produzcan desde la última visualización de la página Edit.

try
{
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
catch (DbUpdateConcurrencyException ex)
{
var exceptionEntry = ex.Entries.Single();
var clientValues = (Department)exceptionEntry.Entity;
var databaseEntry = exceptionEntry.GetDatabaseValues();
if (databaseEntry == null)
{
ModelState.AddModelError(string.Empty, "Unable to save. " +
"The department was deleted by another user.");
return Page();
}

var dbValues = (Department)databaseEntry.ToObject();


await setDbErrorMessage(dbValues, clientValues, _context);

// Save the current RowVersion so next postback


// matches unless an new concurrency issue happens.
Department.RowVersion = (byte[])dbValues.RowVersion;
// Must clear the model error for the next postback.
ModelState.Remove("Department.RowVersion");
}

La instrucción ModelState.Remove es necesaria porque ModelState tiene el valor RowVersion antiguo. En la


página de Razor, el valor ModelState de un campo tiene prioridad sobre los valores de propiedad de modelo
cuando ambos están presentes.

Actualizar la página Edit


Actualice Pages/Departments/Edit.cshtml con el siguiente marcado:
@page "{id:int}"
@model ContosoUniversity.Pages.Departments.EditModel
@{
ViewData["Title"] = "Edit";
}
<h2>Edit</h2>
<h4>Department</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="Department.DepartmentID" />
<input type="hidden" asp-for="Department.RowVersion" />
<div class="form-group">
<label>RowVersion</label>
@Model.Department.RowVersion[7]
</div>
<div class="form-group">
<label asp-for="Department.Name" class="control-label"></label>
<input asp-for="Department.Name" class="form-control" />
<span asp-validation-for="Department.Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Department.Budget" class="control-label"></label>
<input asp-for="Department.Budget" class="form-control" />
<span asp-validation-for="Department.Budget" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Department.StartDate" class="control-label"></label>
<input asp-for="Department.StartDate" class="form-control" />
<span asp-validation-for="Department.StartDate" class="text-danger">
</span>
</div>
<div class="form-group">
<label class="control-label">Instructor</label>
<select asp-for="Department.InstructorID" class="form-control"
asp-items="@Model.InstructorNameSL"></select>
<span asp-validation-for="Department.InstructorID" class="text-danger">
</span>
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</form>
</div>
</div>
<div>
<a asp-page="./Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

El marcado anterior:
Actualiza la directiva page de @page a @page "{id:int}" .
Agrega una versión de fila oculta. Se debe agregar RowVersion para que la devolución enlace el valor.
Muestra el último byte de RowVersion para fines de depuración.
Reemplaza ViewData con InstructorNameSL fuertemente tipadas.

Comprobar los conflictos de simultaneidad con la página Edit


Abra dos instancias de exploradores de Edit en el departamento de inglés:
Ejecute la aplicación y seleccione Departments.
Haga clic con el botón derecho en el hipervínculo Edit del departamento de inglés y seleccione Abrir en
nueva pestaña.
En la primera pestaña, haga clic en el hipervínculo Edit del departamento de inglés.
Las dos pestañas del explorador muestran la misma información.
Cambie el nombre en la primera pestaña del explorador y haga clic en Save.

El explorador muestra la página de índice con el valor modificado y el indicador rowVersion actualizado. Tenga
en cuenta el indicador rowVersion actualizado, que se muestra en el segundo postback en la otra pestaña.
Cambie otro campo en la segunda pestaña del explorador.
Haga clic en Guardar. Verá mensajes de error para todos los campos que no coinciden con los valores de la base
de datos:
Esta ventana del explorador no planeaba cambiar el campo Name. Copie y pegue el valor actual (Languages) en
el campo Name. Presione TAB para salir del campo. La validación del lado cliente quita el mensaje de error.
Vuelva a hacer clic en Save. Se guarda el valor especificado en la segunda pestaña del explorador. Verá los
valores guardados en la página de índice.

Actualizar la página Delete


Actualice el modelo de la página Delete con el código siguiente:

using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Departments
{
public class DeleteModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;
public DeleteModel(ContosoUniversity.Data.SchoolContext context)
{
_context = context;
}

[BindProperty]
public Department Department { get; set; }
public string ConcurrencyErrorMessage { get; set; }

public async Task<IActionResult> OnGetAsync(int id, bool? concurrencyError)


{
Department = await _context.Departments
.Include(d => d.Administrator)
.AsNoTracking()
.FirstOrDefaultAsync(m => m.DepartmentID == id);

if (Department == null)
{
return NotFound();
}

if (concurrencyError.GetValueOrDefault())
{
ConcurrencyErrorMessage = "The record you attempted to delete "
+ "was modified by another user after you selected delete. "
+ "The delete operation was canceled and the current values in the "
+ "database have been displayed. If you still want to delete this "
+ "record, click the Delete button again.";
}
return Page();
}

public async Task<IActionResult> OnPostAsync(int id)


{
try
{
if (await _context.Departments.AnyAsync(
m => m.DepartmentID == id))
{
// Department.rowVersion value is from when the entity
// was fetched. If it doesn't match the DB, a
// DbUpdateConcurrencyException exception is thrown.
_context.Departments.Remove(Department);
await _context.SaveChangesAsync();
}
return RedirectToPage("./Index");
}
catch (DbUpdateConcurrencyException)
{
return RedirectToPage("./Delete",
new { concurrencyError = true, id = id });
}
}
}
}

La página Delete detecta los conflictos de simultaneidad cuando la entidad ha cambiado después de que se
capturase. Department.RowVersion es la versión de fila cuando se capturó la entidad. Cuando EF Core crea el
comando DELETE de SQL, incluye una cláusula WHERE con RowVersion . Si el comando DELETE de SQL tiene
como resultado cero filas afectadas:
La RowVersion del comando DELETE de SQL no coincide con la RowVersion de la base de datos.
Se produce una excepción DbUpdateConcurrencyException.
Se llama a OnGetAsync con el concurrencyError .
Actualizar la página Delete
Actualice Pages/Departments/Delete.cshtml con el código siguiente:

@page "{id:int}"
@model ContosoUniversity.Pages.Departments.DeleteModel

@{
ViewData["Title"] = "Delete";
}

<h2>Delete</h2>

<p class="text-danger">@Model.ConcurrencyErrorMessage</p>

<h3>Are you sure you want to delete this?</h3>


<div>
<h4>Department</h4>
<hr />
<dl class="dl-horizontal">
<dt>
@Html.DisplayNameFor(model => model.Department.Name)
</dt>
<dd>
@Html.DisplayFor(model => model.Department.Name)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Department.Budget)
</dt>
<dd>
@Html.DisplayFor(model => model.Department.Budget)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Department.StartDate)
</dt>
<dd>
@Html.DisplayFor(model => model.Department.StartDate)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Department.RowVersion)
</dt>
<dd>
@Html.DisplayFor(model => model.Department.RowVersion[7])
</dd>
<dt>
@Html.DisplayNameFor(model => model.Department.Administrator)
</dt>
<dd>
@Html.DisplayFor(model => model.Department.Administrator.FullName)
</dd>
</dl>

<form method="post">
<input type="hidden" asp-for="Department.DepartmentID" />
<input type="hidden" asp-for="Department.RowVersion" />
<div class="form-actions no-color">
<input type="submit" value="Delete" class="btn btn-default" /> |
<a asp-page="./Index">Back to List</a>
</div>
</form>
</div>

En el marcado anterior se realizan los cambios siguientes:


Se actualiza la directiva page de @page a @page "{id:int}" .
Se agrega un mensaje de error.
Se reemplaza FirstMidName por FullName en el campo Administrator.
Se cambia RowVersion para que muestre el último byte.
Se agrega una versión de fila oculta. Se debe agregar RowVersion para que la devolución enlace el valor.
Comprobar los conflictos de simultaneidad con la página Delete
Cree un departamento de prueba.
Abra dos instancias de exploradores de Delete en el departamento de prueba:
Ejecute la aplicación y seleccione Departments.
Haga clic con el botón derecho en el hipervínculo Delete del departamento de prueba y seleccione Abrir en
nueva pestaña.
Haga clic en el hipervínculo Edit del departamento de prueba.
Las dos pestañas del explorador muestran la misma información.
Cambie el presupuesto en la primera pestaña del explorador y haga clic en Save.
El explorador muestra la página de índice con el valor modificado y el indicador rowVersion actualizado. Tenga
en cuenta el indicador rowVersion actualizado, que se muestra en el segundo postback en la otra pestaña.
Elimine el departamento de prueba de la segunda pestaña. Se mostrará un error de simultaneidad con los
valores actuales de la base de datos. Al hacer clic en Delete se elimina la entidad, a menos que se haya
actualizado RowVersion . El departamento se ha eliminado.
Vea Herencia para obtener información sobre cómo se hereda un modelo de datos.
Recursos adicionales
Tokens de simultaneidad en EF Core
Controlar la simultaneidad en EF Core

A N T E R IO R
ASP.NET Core MVC con EF Core: serie de tutoriales
21/06/2018 • 2 minutes to read • Edit Online

En este tutorial se explica el funcionamiento de ASP.NET Core MVC y Entity Framework Core con controladores y
vistas. Las páginas de Razor son una nueva alternativa en ASP.NET Core 2.0, un modelo de programación basado
en páginas que facilita la compilación de interfaces de usuario web y hace que sean más productivas.
Recomendamos el tutorial sobre las páginas de Razor antes que la versión MVC. El tutorial de las páginas de
Razor:
Es más fácil de seguir.
Proporciona más procedimientos recomendados de EF Core.
Usa consultas más eficaces.
Es más actual en relación con la API más reciente.
Abarca más características.
Es el método preferido para el desarrollo de nuevas aplicaciones.
Si elige este tutorial en lugar de la versión de páginas de Razor, le agradeceremos que nos explique el motivo en
este problema de GitHub.
1. Introducción
2. Operaciones de creación, lectura, actualización y eliminación
3. Ordenado, filtrado, paginación y agrupación
4. Migraciones
5. Creación de un modelo de datos complejo
6. Lectura de datos relacionados
7. Actualización de datos relacionados
8. Control de conflictos de simultaneidad
9. Herencia
10. Temas avanzados
ASP.NET Core MVC con Entity Framework Core:
Tutorial 1 de 10
25/06/2018 • 40 minutes to read • Edit Online

Por Tom Dykstra y Rick Anderson


En este tutorial se explica el funcionamiento de ASP.NET Core MVC y Entity Framework Core con
controladores y vistas. Las páginas de Razor son una nueva alternativa en ASP.NET Core 2.0, un modelo de
programación basado en páginas que facilita la compilación de interfaces de usuario web y hace que sean más
productivas. Recomendamos el tutorial sobre las páginas de Razor antes que la versión MVC. El tutorial de las
páginas de Razor:
Es más fácil de seguir.
Proporciona más procedimientos recomendados de EF Core.
Usa consultas más eficaces.
Es más actual en relación con la API más reciente.
Abarca más características.
Es el método preferido para el desarrollo de nuevas aplicaciones.
Si elige este tutorial en lugar de la versión de páginas de Razor, le agradeceremos que nos explique el motivo
en este problema de GitHub.
En la aplicación web de ejemplo Contoso University se muestra cómo crear aplicaciones web ASP.NET Core
2.0 MVC con Entity Framework (EF ) Core 2.0 y Visual Studio 2017.
La aplicación de ejemplo es un sitio web de una universidad ficticia, Contoso University. Incluye funciones
como la admisión de estudiantes, la creación de cursos y asignaciones de instructores. Este es el primero de
una serie de tutoriales en los que se explica cómo crear la aplicación de ejemplo Contoso University desde el
principio.
Descargue o vea la aplicación completa.
EF Core 2.0 es la versión más reciente de EF pero aún no dispone de todas las características de EF 6.x. Para
obtener información sobre cómo elegir entre EF 6.x y EF Core, vea Comparar EF Core y EF6.x. Si elige EF 6.x,
vea la versión anterior de esta serie de tutoriales.

NOTE
Para la versión 1.1 de ASP.NET Core de este tutorial, vea la versión VS 2017 Update 2 de este tutorial en formato PDF.

Requisitos previos
Install one of the following:
CLI tooling: Windows, Linux, or macOS: .NET Core SDK 2.0 or later
IDE/editor tooling
Windows: Visual Studio for Windows
ASP.NET and web development workload
.NET Core cross-platform development workload
Linux: Visual Studio Code
macOS: Visual Studio for Mac

Solución de problemas
Si experimenta un problema que no puede resolver, por lo general podrá encontrar la solución si compara el
código con el proyecto completado. Para obtener una lista de errores comunes y cómo resolverlos, vea la
sección de solución de problemas del último tutorial de la serie. Si ahí no encuentra lo que necesita, puede
publicar una pregunta en StackOverflow.com para ASP.NET Core o EF Core.

TIP
Esta es una serie de 10 tutoriales y cada uno se basa en lo que se realiza en los anteriores. Considere la posibilidad de
guardar una copia del proyecto después de completar correctamente cada tutorial. Después, si experimenta problemas,
puede empezar desde el tutorial anterior en lugar de volver al principio de la serie completa.

La aplicación web Contoso University


La aplicación que se va a compilar en estos tutoriales es un sitio web sencillo de una universidad.
Los usuarios pueden ver y actualizar la información de estudiantes, cursos e instructores. A continuación se
muestran algunas de las pantallas que se van a crear.
El estilo de la interfaz de usuario de este sitio se ha mantenido fiel a lo que generan las plantillas integradas,
para que el tutorial se pueda centrar principalmente en cómo usar Entity Framework.

Creación de una aplicación web ASP.NET Core MVC


Abra Visual Studio y cree un proyecto web de ASP.NET Core C# con el nombre "ContosoUniversity".
En el menú Archivo, seleccione Nuevo > Proyecto.
En el panel de la izquierda, seleccione Instalado > Visual C# > Web.
Seleccione la plantilla de proyecto Aplicación web ASP.NET Core.
Escriba ContosoUniversity como el nombre y haga clic en Aceptar.
Espere que aparezca el cuadro de diálogo Nueva aplicación web ASP.NET Core (.NET Core).
Seleccione ASP.NET Core 2.0 y la plantilla Aplicación web (controlador de vista de modelos).
Nota: Este tutorial requiere ASP.NET Core 2.0 y EF Core 2.0 o una versión posterior; asegúrese de que
ASP.NET Core 1.1 no está seleccionado.
Asegúrese de que Autenticación esté establecida en Sin autenticación.
Haga clic en Aceptar.

Configurar el estilo del sitio


Con algunos cambios sencillos se configura el menú del sitio, el diseño y la página principal.
Abra Views/Shared/_Layout.cshtml y realice los cambios siguientes:
Cambie todas las repeticiones de "ContosoUniversity" por "Contoso University". Hay tres repeticiones.
Agregue entradas de menú para Students, Courses, Instructors y Departments, y elimine la entrada
de menú Contact.
Los cambios aparecen resaltados.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - Contoso University</title>

<environment names="Development">
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
<link rel="stylesheet" href="~/css/site.css" />
</environment>
<environment names="Staging,Production">
<link rel="stylesheet" href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.min.css"
asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-
value="absolute" />
value="absolute" />
<link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" />
</environment>

</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-
collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a asp-area="" asp-controller="Home" asp-action="Index" class="navbar-brand">Contoso
University</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a asp-area="" asp-controller="Home" asp-action="Index">Home</a></li>
<li><a asp-area="" asp-controller="Home" asp-action="About">About</a></li>
<li><a asp-area="" asp-controller="Students" asp-action="Index">Students</a></li>
<li><a asp-area="" asp-controller="Courses" asp-action="Index">Courses</a></li>
<li><a asp-area="" asp-controller="Instructors" asp-action="Index">Instructors</a></li>
<li><a asp-area="" asp-controller="Departments" asp-action="Index">Departments</a></li>
</ul>
</div>
</div>
</nav>
<div class="container body-content">
@RenderBody()
<hr />
<footer>
<p>&copy; 2017 - Contoso University</p>
</footer>
</div>

<environment names="Development">
<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
</environment>
<environment names="Staging,Production">
<script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-2.2.0.min.js"
asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
asp-fallback-test="window.jQuery"
crossorigin="anonymous"
integrity="sha384-K+ctZQ+LL8q6tP7I94W+qzQsfRV2a+AfHIi9k8z8l9ggpc8X+Ytst4yBo/hH+8Fk">
</script>
<script src="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/bootstrap.min.js"
asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.min.js"
asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal"
crossorigin="anonymous"
integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa">
</script>
<script src="~/js/site.min.js" asp-append-version="true"></script>
</environment>

@RenderSection("Scripts", required: false)


</body>
</html>

En Views/Home/Index.cshtml, reemplace el contenido del archivo con el código siguiente para reemplazar el
texto sobre ASP.NET y MVC con texto sobre esta aplicación:
@{
ViewData["Title"] = "Home Page";
}

<div class="jumbotron">
<h1>Contoso University</h1>
</div>
<div class="row">
<div class="col-md-4">
<h2>Welcome to Contoso University</h2>
<p>
Contoso University is a sample application that
demonstrates how to use Entity Framework Core in an
ASP.NET Core MVC web application.
</p>
</div>
<div class="col-md-4">
<h2>Build it from scratch</h2>
<p>You can build the application by following the steps in a series of tutorials.</p>
<p><a class="btn btn-default" href="https://docs.asp.net/en/latest/data/ef-mvc/intro.html">See the
tutorial &raquo;</a></p>
</div>
<div class="col-md-4">
<h2>Download it</h2>
<p>You can download the completed project from GitHub.</p>
<p><a class="btn btn-default" href="https://github.com/aspnet/Docs/tree/master/aspnetcore/data/ef-
mvc/intro/samples/cu-final">See project source code &raquo;</a></p>
</div>
</div>

Presione CTRL+F5 para ejecutar el proyecto o seleccione Depurar > Iniciar sin depurar en el menú. Verá la
página principal con pestañas para las páginas que se crearán en estos tutoriales.
Paquetes NuGet de Entity Framework Core
Para agregar compatibilidad con EF Core a un proyecto, instale el proveedor de base de datos que quiera tener
como destino. En este tutorial se usa SQL Server y el paquete de proveedor es
Microsoft.EntityFrameworkCore.SqlServer. Este paquete se incluye en el metapaquete
Microsoft.AspNetCore.All, por lo que no es necesario instalarlo.
Este paquete y sus dependencias ( Microsoft.EntityFrameworkCore y Microsoft.EntityFrameworkCore.Relational )
proporcionan compatibilidad en tiempo de ejecución para EF. Más adelante, en el tutorial Migraciones,
agregará un paquete de herramientas.
Para obtener información sobre otros proveedores de base de datos disponibles para Entity Framework Core,
vea Proveedores de bases de datos.

Crear el modelo de datos


A continuación podrá crear las clases de entidad para la aplicación Contoso University. Empezará por las tres
siguientes entidades.
Hay una relación uno a varios entre las entidades Student y Enrollment , y también entre las entidades
Course y Enrollment . En otras palabras, un estudiante se puede inscribir en cualquier número de cursos y un
curso puede tener cualquier número de alumnos inscritos.
En las secciones siguientes creará una clase para cada una de estas entidades.
La entidad Student

En la carpeta Models, cree un archivo de clase denominado Student.cs y reemplace el código de plantilla con el
código siguiente.

using System;
using System.Collections.Generic;

namespace ContosoUniversity.Models
{
public class Student
{
public int ID { get; set; }
public string LastName { get; set; }
public string FirstMidName { get; set; }
public DateTime EnrollmentDate { get; set; }

public ICollection<Enrollment> Enrollments { get; set; }


}
}

La propiedad ID se convertirá en la columna de clave principal de la tabla de base de datos que corresponde
a esta clase. De forma predeterminada, Entity Framework interpreta como la clave principal una propiedad que
se denomine ID o classnameID .
La propiedad Enrollments es una propiedad de navegación. Las propiedades de navegación contienen otras
entidades relacionadas con esta entidad. En este caso, la propiedad Enrollments de una Student entity
contendrá todas las entidades Enrollment que estén relacionadas con esa entidad Student . En otras palabras,
si una fila Student determinada en la base de datos tiene dos filas Enrollment relacionadas (filas que contienen
el valor de clave principal de ese estudiante en la columna de clave externa StudentID ), la propiedad de
navegación Enrollments de esa entidad Student contendrá esas dos entidades Enrollment .
Si una propiedad de navegación puede contener varias entidades (como en las relaciones de varios a varios o
uno a varios), su tipo debe ser una lista a la que se puedan agregar las entradas, eliminarlas y actualizarlas,
como ICollection<T> . Puede especificar ICollection<T> o un tipo como List<T> o HashSet<T> . Si especifica
ICollection<T> , EF crea una colección HashSet<T> de forma predeterminada.

La entidad Enrollment

En la carpeta Models, cree Enrollment.cs y reemplace el código existente con el código siguiente:

namespace ContosoUniversity.Models
{
public enum Grade
{
A, B, C, D, F
}

public class Enrollment


{
public int EnrollmentID { get; set; }
public int CourseID { get; set; }
public int StudentID { get; set; }
public Grade? Grade { get; set; }

public Course Course { get; set; }


public Student Student { get; set; }
}
}

La propiedad EnrollmentID será la clave principal; esta entidad usa el patrón classnameID en lugar de ID por
sí solo, como se vio en la entidad Student . Normalmente debería elegir un patrón y usarlo en todo el modelo
de datos. En este caso, la variación muestra que se puede usar cualquiera de los patrones. En un tutorial
posterior, verá cómo el uso de ID sin un nombre de clase facilita la implementación de la herencia en el
modelo de datos.
La propiedad Grade es una enum . El signo de interrogación después de la declaración de tipo Grade indica
que la propiedad Grade acepta valores NULL. Una calificación que sea NULL es diferente de una calificación
que sea cero; NULL significa que no se conoce una calificación o que todavía no se ha asignado.
La propiedad StudentID es una clave externa y la propiedad de navegación correspondiente es Student . Una
entidad Enrollment está asociada con una entidad Student , por lo que la propiedad solo puede contener un
única entidad Student (a diferencia de la propiedad de navegación Student.Enrollments que se vio
anteriormente, que puede contener varias entidades Enrollment ).
La propiedad CourseID es una clave externa y la propiedad de navegación correspondiente es Course . Una
entidad Enrollment está asociada con una entidad Course .
Entity Framework interpreta una propiedad como propiedad de clave externa si se denomina
<navigation property name><primary key property name> (por ejemplo StudentID para la propiedad de
navegación Student , dado que la clave principal de la entidad Student es ID ). Las propiedades de clave
externa también se pueden denominar simplemente <primary key property name> (por ejemplo CourseID ,
dado que la clave principal de la entidad Course es CourseID ).
La entidad Course

En la carpeta Models, cree Course.cs y reemplace el código existente con el código siguiente:

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
public class Course
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int CourseID { get; set; }
public string Title { get; set; }
public int Credits { get; set; }

public ICollection<Enrollment> Enrollments { get; set; }


}
}

La propiedad Enrollments es una propiedad de navegación. Una entidad Course puede estar relacionada con
cualquier número de entidades Enrollment .
En un tutorial posterior de esta serie se incluirá más información sobre el atributo DatabaseGenerated .
Básicamente, este atributo permite escribir la clave principal para el curso en lugar de hacer que la base de
datos lo genere.

Crear el contexto de base de datos


La clase principal que coordina la funcionalidad de Entity Framework para un modelo de datos determinado es
la clase de contexto de base de datos. Esta clase se crea al derivar de la clase
Microsoft.EntityFrameworkCore.DbContext . En el código se especifica qué entidades se incluyen en el modelo de
datos. También se puede personalizar determinado comportamiento de Entity Framework. En este proyecto, la
clase se denomina SchoolContext .
En la carpeta del proyecto, cree una carpeta denominada Data.
En la carpeta Data, cree un archivo de clase denominado SchoolContext.cs y reemplace el código de plantilla
con el código siguiente:
using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity.Data
{
public class SchoolContext : DbContext
{
public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
{
}

public DbSet<Course> Courses { get; set; }


public DbSet<Enrollment> Enrollments { get; set; }
public DbSet<Student> Students { get; set; }
}
}

Este código crea una propiedad DbSet para cada conjunto de entidades. En la terminología de Entity
Framework, un conjunto de entidades suele corresponderse con una tabla de base de datos, mientras que una
entidad lo hace con una fila de la tabla.
Se podrían haber omitido las instrucciones DbSet<Enrollment> y DbSet<Course> , y el funcionamiento sería el
mismo. Entity Framework las incluiría implícitamente porque la entidad Student hace referencia a la entidad
Enrollment y la entidad Enrollment hace referencia a la entidad Course .

Cuando se crea la base de datos, EF crea las tablas con los mismos nombres que los nombres de propiedad
DbSet . Los nombres de propiedad para las colecciones normalmente están en plural ( Students en lugar de
Student), pero los desarrolladores no están de acuerdo sobre si los nombres de tabla deben estar en plural o
no. Para estos tutoriales, se invalidará el comportamiento predeterminado mediante la especificación de
nombres de tabla en singular en DbContext. Para ello, agregue el código resaltado siguiente después de la
última propiedad DbSet.

using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity.Data
{
public class SchoolContext : DbContext
{
public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
{
}

public DbSet<Course> Courses { get; set; }


public DbSet<Enrollment> Enrollments { get; set; }
public DbSet<Student> Students { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)


{
modelBuilder.Entity<Course>().ToTable("Course");
modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
modelBuilder.Entity<Student>().ToTable("Student");
}
}
}

Registro del contexto con inserción de dependencias


ASP.NET Core implementa la inserción de dependencias de forma predeterminada. Los servicios (como el
contexto de base de datos de EF ) se registran con inserción de dependencias durante el inicio de la aplicación.
Estos servicios se proporcionan a los componentes que los necesitan (como los controladores MVC ) a través
de parámetros de constructor. Más adelante en este tutorial verá el código de constructor de controlador que
obtiene una instancia de contexto.
Para registrar SchoolContext como servicio, abra Startup.cs y agregue las líneas resaltadas al método
ConfigureServices .

public void ConfigureServices(IServiceCollection services)


{
services.AddDbContext<SchoolContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

services.AddMvc();
}

El nombre de la cadena de conexión se pasa al contexto mediante una llamada a un método en un objeto
DbContextOptionsBuilder . Para el desarrollo local, el sistema de configuración de ASP.NET Core lee la cadena
de conexión desde el archivo appsettings.json.
Agregue instrucciones para los espacios de nombres ContosoUniversity.Data y
using
Microsoft.EntityFrameworkCore , y después compile el proyecto.

using ContosoUniversity.Data;
using Microsoft.EntityFrameworkCore;

Abra el archivo appsettings.json y agregue una cadena de conexión como se muestra en el ejemplo siguiente.

{
"ConnectionStrings": {
"DefaultConnection": "Server=
(localdb)\\mssqllocaldb;Database=ContosoUniversity1;Trusted_Connection=True;MultipleActiveResultSets=true"
},
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Warning"
}
}
}

SQL Server Express LocalDB


La cadena de conexión especifica una base de datos de SQL Server LocalDB. LocalDB es una versión ligera del
motor de base de datos de SQL Server Express que está dirigida al desarrollo de aplicaciones, no al uso en
producción. LocalDB se inicia a petición y se ejecuta en modo de usuario, sin necesidad de una configuración
compleja. De forma predeterminada, LocalDB crea archivos de base de datos .mdf en el directorio
C:/Users/<user> .

Agregue código para inicializar la base de datos con datos de prueba


Entity Framework creará una base de datos vacía por usted. En esta sección, escribirá un método que se llama
después de crear la base de datos para rellenarla con datos de prueba.
Aquí usará el método EnsureCreated para crear automáticamente la base de datos. En un tutorial posterior,
verá cómo controlar los cambios en el modelo mediante Migraciones de Code First para cambiar el esquema
de base de datos en lugar de quitar y volver a crear la base de datos.
En la carpeta Data, cree un archivo de clase denominado DbInitializer.cs y reemplace el código de plantilla con
el código siguiente, que hace que se cree una base de datos cuando es necesario y carga datos de prueba en la
nueva base de datos.

using ContosoUniversity.Models;
using System;
using System.Linq;

namespace ContosoUniversity.Data
{
public static class DbInitializer
{
public static void Initialize(SchoolContext context)
{
context.Database.EnsureCreated();

// Look for any students.


if (context.Students.Any())
{
return; // DB has been seeded
}

var students = new Student[]


{
new Student{FirstMidName="Carson",LastName="Alexander",EnrollmentDate=DateTime.Parse("2005-09-
01")},
new Student{FirstMidName="Meredith",LastName="Alonso",EnrollmentDate=DateTime.Parse("2002-09-
01")},
new Student{FirstMidName="Arturo",LastName="Anand",EnrollmentDate=DateTime.Parse("2003-09-
01")},
new Student{FirstMidName="Gytis",LastName="Barzdukas",EnrollmentDate=DateTime.Parse("2002-09-
01")},
new Student{FirstMidName="Yan",LastName="Li",EnrollmentDate=DateTime.Parse("2002-09-01")},
new Student{FirstMidName="Peggy",LastName="Justice",EnrollmentDate=DateTime.Parse("2001-09-
01")},
new Student{FirstMidName="Laura",LastName="Norman",EnrollmentDate=DateTime.Parse("2003-09-
01")},
new Student{FirstMidName="Nino",LastName="Olivetto",EnrollmentDate=DateTime.Parse("2005-09-
01")}
};
foreach (Student s in students)
{
context.Students.Add(s);
}
context.SaveChanges();

var courses = new Course[]


{
new Course{CourseID=1050,Title="Chemistry",Credits=3},
new Course{CourseID=4022,Title="Microeconomics",Credits=3},
new Course{CourseID=4041,Title="Macroeconomics",Credits=3},
new Course{CourseID=1045,Title="Calculus",Credits=4},
new Course{CourseID=3141,Title="Trigonometry",Credits=4},
new Course{CourseID=2021,Title="Composition",Credits=3},
new Course{CourseID=2042,Title="Literature",Credits=4}
};
foreach (Course c in courses)
{
context.Courses.Add(c);
}
context.SaveChanges();

var enrollments = new Enrollment[]


{
new Enrollment{StudentID=1,CourseID=1050,Grade=Grade.A},
new Enrollment{StudentID=1,CourseID=4022,Grade=Grade.C},
new Enrollment{StudentID=1,CourseID=4041,Grade=Grade.B},
new Enrollment{StudentID=2,CourseID=1045,Grade=Grade.B},
new Enrollment{StudentID=2,CourseID=3141,Grade=Grade.F},
new Enrollment{StudentID=2,CourseID=3141,Grade=Grade.F},
new Enrollment{StudentID=2,CourseID=2021,Grade=Grade.F},
new Enrollment{StudentID=3,CourseID=1050},
new Enrollment{StudentID=4,CourseID=1050},
new Enrollment{StudentID=4,CourseID=4022,Grade=Grade.F},
new Enrollment{StudentID=5,CourseID=4041,Grade=Grade.C},
new Enrollment{StudentID=6,CourseID=1045},
new Enrollment{StudentID=7,CourseID=3141,Grade=Grade.A},
};
foreach (Enrollment e in enrollments)
{
context.Enrollments.Add(e);
}
context.SaveChanges();
}
}
}

El código comprueba si hay estudiantes en la base de datos, y si no es así, asume que la base de datos es nueva
y debe inicializarse con datos de prueba. Carga los datos de prueba en matrices en lugar de colecciones
List<T> para optimizar el rendimiento.

En Program.cs, modifique el método Main para que haga lo siguiente al iniciar la aplicación:
Obtener una instancia del contexto de base de datos desde el contenedor de inserción de dependencias.
Llamar al método de inicialización, pasándolo al contexto.
Eliminar el contexto cuando el método de inicialización haya finalizado.

public static void Main(string[] args)


{
var host = BuildWebHost(args);

using (var scope = host.Services.CreateScope())


{
var services = scope.ServiceProvider;
try
{
var context = services.GetRequiredService<SchoolContext>();
DbInitializer.Initialize(context);
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred while seeding the database.");
}
}

host.Run();
}

Agregue instrucciones using :

using Microsoft.Extensions.DependencyInjection;
using ContosoUniversity.Data;

En los tutoriales anteriores, es posible que vea código similar en el método Configure de Startup.cs. Se
recomienda usar el método Configure solo para configurar la canalización de solicitudes. El código de inicio
de la aplicación pertenece al método Main .
Ahora, la primera vez que ejecute la aplicación, se creará la base de datos y se inicializará con datos de prueba.
Cada vez que cambie el modelo de datos, puede eliminar la base de datos, actualizar el método de
inicialización y comenzar desde cero con una base de datos nueva del mismo modo. En los tutoriales
posteriores, verá cómo modificar la base de datos cuando cambie el modelo de datos, sin tener que eliminarla
y volver a crearla.

Crear un controlador y vistas


A continuación, usará el motor de scaffolding de Visual Studio para agregar un controlador y vistas de MVC
que usarán EF para consultar y guardar los datos.
La creación automática de vistas y métodos de acción CRUD se conoce como scaffolding. El scaffolding difiere
de la generación de código en que el código con scaffolding es un punto de partida que se puede modificar
para satisfacer sus propias necesidades, mientras que el código generado normalmente no se modifica.
Cuando tenga que personalizar código generado, use clases parciales o regenere el código cuando se
produzcan cambios.
Haga clic con el botón derecho en la carpeta Controladores en el Explorador de soluciones y seleccione
Agregar > Nuevo elemento con scaffold.
Si aparece el cuadro de diálogo Agregar dependencias de MVC:
Actualice Visual Studio a la última versión. La versiones de Visual Studio anteriores a la 15.5 muestran
este cuadro de diálogo.
Si no puede actualizar, seleccione AGREGAR y luego siga los pasos para agregar el controlador de
nuevo.
En el cuadro de diálogo Agregar scaffold:
Seleccione Controlador de MVC con vistas que usan Entity Framework.
Haga clic en Agregar.
En el cuadro de diálogo Agregar controlador:
En Clase de modelo seleccione Student.
En Clase de contexto de datos seleccione SchoolContext.
Acepte el valor predeterminado StudentsController como el nombre.
Haga clic en Agregar.

Al hacer clic en Agregar, el motor de scaffolding de Visual Studio crea un archivo StudentsController.cs
y un conjunto de vistas (archivos .cshtml) que funcionan con el controlador.
(El motor de scaffolding también puede crear el contexto de base de datos de forma automática si no lo crea
primero manualmente como se hizo antes en este tutorial. Puede especificar una clase de contexto nueva en el
cuadro Agregar controlador si hace clic en el signo más situado a la derecha de Clase del contexto de
datos. Después, Visual Studio creará la clase DbContext , así como el controlador y las vistas).
Observará que el controlador toma un SchoolContext como parámetro de constructor.

namespace ContosoUniversity.Controllers
{
public class StudentsController : Controller
{
private readonly SchoolContext _context;

public StudentsController(SchoolContext context)


{
_context = context;
}

La inserción de dependencias de ASP.NET se encargará de pasar una instancia de SchoolContext al


controlador. Lo configuró anteriormente en el archivo Startup.cs.
El controlador contiene un método de acción Index , que muestra todos los alumnos en la base de datos. El
método obtiene una lista de estudiantes de la entidad Students, que se establece leyendo la propiedad
Students de la instancia del contexto de base de datos:

public async Task<IActionResult> Index()


{
return View(await _context.Students.ToListAsync());
}

Más adelante en el tutorial obtendrá información sobre los elementos de programación asincrónicos de este
código.
En la vista Views/Students/Index.cshtml se muestra esta lista en una tabla:
@model IEnumerable<ContosoUniversity.Models.Student>

@{
ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.LastName)
</th>
<th>
@Html.DisplayNameFor(model => model.FirstMidName)
</th>
<th>
@Html.DisplayNameFor(model => model.EnrollmentDate)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.LastName)
</td>
<td>
@Html.DisplayFor(modelItem => item.FirstMidName)
</td>
<td>
@Html.DisplayFor(modelItem => item.EnrollmentDate)
</td>
<td>
<a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-action="Details" asp-route-id="@item.ID">Details</a> |
<a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>

Presione CTRL+F5 para ejecutar el proyecto o seleccione Depurar > Iniciar sin depurar en el menú.
Haga clic en la pestaña Students para ver los datos de prueba insertados por el método
DbInitializer.Initialize . En función del ancho de la ventana del explorador, verá el vínculo de la pestaña
Student en la parte superior de la página o tendrá que hacer clic en el icono de navegación en la esquina
superior derecha para verlo.
Ver la base de datos
Al iniciar la aplicación, el método DbInitializer.Initialize llama a EnsureCreated . EF comprobó que no había
ninguna base de datos y creó una, y después el resto del código del método Initialize la rellenó con datos.
Puede usar el Explorador de objetos de SQL Server (SSOX) para ver la base de datos en Visual Studio.
Cierre el explorador.
Si la ventana de SSOX no está abierta, selecciónela en el menú Vista de Visual Studio.
En SSOX, haga clic en (localdb)\MSSQLLocalDB > Databases y después en la entrada del nombre de base
de datos que se encuentra en la cadena de conexión del archivo appsettings.json.
Expanda el nodo Tablas para ver las tablas de la base de datos.
Haga clic con el botón derecho en la tabla Student y haga clic en Ver datos para ver las columnas que se
crearon y las filas que se insertaron en la tabla.

Los archivos de base de datos .mdf y .ldf se encuentran en la carpeta C:\Usuarios\.


Como se está llamando a EnsureCreated en el método de inicializador que se ejecuta al iniciar la aplicación,
ahora podría realizar un cambio en la clase Student , eliminar la base de datos, volver a ejecutar la aplicación y
la base de datos se volvería a crear de forma automática para que coincida con el cambio. Por ejemplo, si
agrega una propiedad EmailAddress a la clase Student , verá una columna EmailAddress nueva en la tabla que
se ha vuelto a crear.

Convenciones
La cantidad de código que tendría que escribir para que Entity Framework pudiera crear una base de datos
completa para usted es mínima debido al uso de convenciones o las suposiciones que hace Entity Framework.
Los nombres de las propiedades DbSet se usan como nombres de tabla. Para las entidades a las que no
se hace referencia con una propiedad DbSet , los nombres de clase de entidad se usan como nombres
de tabla.
Los nombres de propiedad de entidad se usan para los nombres de columna.
Las propiedades de entidad que se denominan ID o classnameID se reconocen como propiedades de
clave principal.
Una propiedad se interpreta como propiedad de clave externa si se denomina (por ejemplo, StudentID
para la propiedad de navegación Student , dado que la clave principal de la entidad Student es ID ).
Las propiedades de clave externa también se pueden denominar simplemente (por ejemplo
EnrollmentID , dado que la clave principal de la entidad Enrollment es EnrollmentID ).

El comportamiento de las convenciones se puede reemplazar. Por ejemplo, puede especificar explícitamente los
nombres de tabla, como se vio anteriormente en este tutorial. Y puede establecer los nombres de columna y
cualquier propiedad como clave principal o clave externa, como verá en un tutorial posterior de esta serie.

Código asincrónico
La programación asincrónica es el modo predeterminado de ASP.NET Core y EF Core.
Un servidor web tiene un número limitado de subprocesos disponibles y, en situaciones de carga alta, es
posible que todos los subprocesos disponibles estén en uso. Cuando esto ocurre, el servidor no puede
procesar nuevas solicitudes hasta que los subprocesos se liberen. Con el código sincrónico, se pueden
acumular muchos subprocesos mientras no estén realizando ningún trabajo porque están a la espera de que
finalice la E/S. Con el código asincrónico, cuando un proceso está a la espera de que finalice la E/S, se libera su
subproceso para el que el servidor lo use para el procesamiento de otras solicitudes. Como resultado, el código
asincrónico permite que los recursos de servidor se usen de forma más eficaz, y el servidor está habilitado
para administrar más tráfico sin retrasos.
El código asincrónico introduce una pequeña cantidad de sobrecarga en tiempo de ejecución, pero para
situaciones de poco tráfico la disminución del rendimiento es insignificante, mientras que en situaciones de
tráfico elevado, la posible mejora del rendimiento es importante.
En el código siguiente, la palabra clave async , el valor devuelto Task<T> , la palabra clave await y el método
ToListAsync hacen que el código se ejecute de forma asincrónica.

public async Task<IActionResult> Index()


{
return View(await _context.Students.ToListAsync());
}

La palabra clave async indica al compilador que genere devoluciones de llamada para partes del
cuerpo del método y que cree automáticamente el objeto Task<IActionResult> que se devuelve.
El tipo de valor devuelto Task<IActionResult> representa el trabajo en curso con un resultado de tipo
IActionResult .

La palabra clave await hace que el compilador divida el método en dos partes. La primera parte
termina con la operación que se inició de forma asincrónica. La segunda parte se coloca en un método
de devolución de llamada que se llama cuando finaliza la operación.
ToListAsync es la versión asincrónica del método de extensión ToList .
Algunos aspectos que tener en cuenta al escribir código asincrónico en el que se usa Entity Framework son los
siguientes:
Solo se ejecutan de forma asincrónica las instrucciones que hacen que las consultas o los comandos se
envíen a la base de datos. Eso incluye, por ejemplo, ToListAsync , SingleOrDefaultAsync y
SaveChangesAsync . No incluye, por ejemplo, instrucciones que solo cambian una IQueryable , como
var students = context.Students.Where(s => s.LastName == "Davolio") .

Un contexto de EF no es seguro para subprocesos: no intente realizar varias operaciones en paralelo.


Cuando llame a cualquier método asincrónico de EF, use siempre la palabra clave await .
Si quiere aprovechar las ventajas de rendimiento del código asincrónico, asegúrese de que en los
paquetes de biblioteca que use (por ejemplo para paginación), también se usa async si llaman a
cualquier método de Entity Framework que haga que las consultas se envíen a la base de datos.
Para obtener más información sobre la programación asincrónica en .NET, vea Información general de Async.
Resumen
Ha creado una aplicación simple en la que se usa Entity Framework Core y SQL Server Express LocalDB para
almacenar y mostrar datos. En el tutorial siguiente, obtendrá información sobre cómo realizar operaciones
CRUD (crear, leer, actualizar y eliminar) básicas.

S IG U IE N T E
ASP.NET Core MVC con EF Core: CRUD (2 de 10)
25/06/2018 • 38 minutes to read • Edit Online

Por Tom Dykstra y Rick Anderson


En la aplicación web de ejemplo Contoso University se muestra cómo crear aplicaciones web de ASP.NET Core
MVC con Entity Framework Core y Visual Studio. Para obtener información sobre la serie de tutoriales, consulte
el primer tutorial de la serie.
En el tutorial anterior, creó una aplicación MVC que almacena y muestra los datos con Entity Framework y SQL
Server LocalDB. En este tutorial, podrá revisar y personalizar el código CRUD (crear, leer, actualizar y eliminar)
que el scaffolding de MVC crea automáticamente para usted en controladores y vistas.

NOTE
Es una práctica habitual implementar el modelo de repositorio con el fin de crear una capa de abstracción entre el
controlador y la capa de acceso a datos. Para que estos tutoriales sean sencillos y se centren en enseñar a usar Entity
Framework, no se usan repositorios. Para obtener información sobre los repositorios con EF, vea el último tutorial de esta
serie.

En este tutorial, trabajará con las páginas web siguientes:


Personalizar la página de detalles
En el código con scaffolding de la página Students Index se excluyó la propiedad Enrollments porque contiene
una colección. En la página Details, se mostrará el contenido de la colección en una tabla HTML.
En Controllers/StudentsController.cs, el método de acción para la vista Details usa el método
SingleOrDefaultAsync para recuperar una única entidad Student . Agregue código para llamar a los métodos
Include , ThenInclude y AsNoTracking , como se muestra en el siguiente código resaltado.

public async Task<IActionResult> Details(int? id)


{
if (id == null)
{
return NotFound();
}

var student = await _context.Students


.Include(s => s.Enrollments)
.ThenInclude(e => e.Course)
.AsNoTracking()
.SingleOrDefaultAsync(m => m.ID == id);

if (student == null)
{
return NotFound();
}

return View(student);
}

Los métodos Include y ThenInclude hacen que el contexto cargue la propiedad de navegación
Student.Enrollments y, dentro de cada inscripción, la propiedad de navegación Enrollment.Course . Obtendrá más
información sobre estos métodos en el tutorial de lectura de datos relacionados.
El método AsNoTracking mejora el rendimiento en casos en los que no se actualizarán las entidades devueltas en
la duración del contexto actual. Obtendrá más información sobre AsNoTracking al final de este tutorial.
Datos de ruta
El valor de clave que se pasa al método Details procede de los datos de ruta. Los datos de ruta son los que el
enlazador de modelos encuentra en un segmento de la dirección URL. Por ejemplo, la ruta predeterminada
especifica los segmentos de controlador, acción e identificador:

app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});

En la dirección URL siguiente, la ruta predeterminada asigna Instructor como el controlador, Index como la
acción y 1 como el identificador; estos son los valores de datos de ruta.

http://localhost:1230/Instructor/Index/1?courseID=2021

La última parte de la dirección URL ("?courseID=2021") es un valor de cadena de consulta. El enlazador de


modelos también pasará el valor ID al parámetro id del método Details si se pasa como un valor de cadena
de consulta:

http://localhost:1230/Instructor/Index?id=1&CourseID=2021

En la página Index, las instrucciones de la aplicación auxiliar de etiquetas crean direcciones URL de hipervínculo
en la vista de Razor. En el siguiente código de Razor, el parámetro id coincide con la ruta predeterminada, por lo
que se agrega id a los datos de ruta.

<a asp-action="Edit" asp-route-id="@item.ID">Edit</a>

Esto genera el siguiente código HTML cuando item.ID es 6:

<a href="/Students/Edit/6">Edit</a>

En el siguiente código de Razor, studentID no coincide con un parámetro en la ruta predeterminada, por lo que
se agrega como una cadena de consulta.

<a asp-action="Edit" asp-route-studentID="@item.ID">Edit</a>

Esto genera el siguiente código HTML cuando item.ID es 6:

<a href="/Students/Edit?studentID=6">Edit</a>

Para obtener más información sobre las aplicaciones auxiliares de etiquetas, vea Aplicaciones auxiliares de
etiquetas en ASP.NET Core.
Agregar inscripciones a la vista de detalles
Abra Views/Students/Details.cshtml. Cada campo se muestra mediante las aplicaciones auxiliares DisplayNameFor
y DisplayFor , como se muestra en el ejemplo siguiente:

<dt>
@Html.DisplayNameFor(model => model.LastName)
</dt>
<dd>
@Html.DisplayFor(model => model.LastName)
</dd>

Después del último campo e inmediatamente antes de la etiqueta </dl> de cierre, agregue el código siguiente
para mostrar una lista de las inscripciones:

<dt>
@Html.DisplayNameFor(model => model.Enrollments)
</dt>
<dd>
<table class="table">
<tr>
<th>Course Title</th>
<th>Grade</th>
</tr>
@foreach (var item in Model.Enrollments)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Course.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.Grade)
</td>
</tr>
}
</table>
</dd>

Si la sangría de código no es correcta después de pegar el código, presione CTRL -K-D para corregirlo.
Este código recorre en bucle las entidades en la propiedad de navegación Enrollments . Para cada inscripción, se
muestra el título del curso y la calificación. El título del curso se recupera de la entidad Course almacenada en la
propiedad de navegación Course de la entidad Enrollments.
Ejecute la aplicación, haga clic en la pestaña Students y después en el vínculo Details de un estudiante. Verá la
lista de cursos y calificaciones para el alumno seleccionado:
Actualizar la página Create
En StudentsController.cs, modifique el método HttpPost Create agregando un bloque try-catch y quitando ID
del atributo Bind .

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(
[Bind("EnrollmentDate,FirstMidName,LastName")] Student student)
{
try
{
if (ModelState.IsValid)
{
_context.Add(student);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
}
catch (DbUpdateException /* ex */)
{
//Log the error (uncomment ex variable name and write a log.
ModelState.AddModelError("", "Unable to save changes. " +
"Try again, and if the problem persists " +
"see your system administrator.");
}
return View(student);
}

Este código agrega la entidad Student creada por el enlazador de modelos de ASP.NET MVC al conjunto de
entidades Students y después guarda los cambios en la base de datos. (El enlazador de modelos se refiere a la
funcionalidad de ASP.NET MVC que facilita trabajar con datos enviados por un formulario; un enlazador de
modelos convierte los valores de formulario enviados en tipos CLR y los pasa al método de acción en
parámetros. En este caso, el enlazador de modelos crea instancias de una entidad Student mediante valores de
propiedad de la colección Form).
Se ha quitado ID del atributo Bind porque ID es el valor de clave principal que SQL Server establecerá
automáticamente cuando se inserte la fila. La entrada del usuario no establece el valor ID.
Aparte del atributo Bind , el bloque try-catch es el único cambio que se ha realizado en el código con scaffolding.
Si se detecta una excepción derivada de DbUpdateException mientras se guardan los cambios, se muestra un
mensaje de error genérico. En ocasiones, las excepciones DbUpdateException se deben a algo externo a la
aplicación y no a un error de programación, por lo que se recomienda al usuario que vuelva a intentarlo. Aunque
no se ha implementado en este ejemplo, en una aplicación de producción de calidad se debería registrar la
excepción. Para obtener más información, vea la sección Registro para obtener información de Supervisión y
telemetría (creación de aplicaciones de nube reales con Azure).
El atributo ValidateAntiForgeryToken ayuda a evitar ataques de falsificación de solicitud entre sitios (CSRF ). El
token se inserta automáticamente en la vista por medio de FormTagHelper y se incluye cuando el usuario envía
el formulario. El token se valida mediante el atributo ValidateAntiForgeryToken . Para obtener más información
sobre CSRF, vea Prevención de ataques de falsificación de solicitud.
Nota de seguridad sobre la publicación excesiva
El atributo Bind que el código con scaffolding incluye en el método Create es una manera de protegerse contra
la publicación excesiva en escenarios de creación. Por ejemplo, suponga que la entidad Student incluye una
propiedad Secret que no quiere que esta página web establezca.

public class Student


{
public int ID { get; set; }
public string LastName { get; set; }
public string FirstMidName { get; set; }
public DateTime EnrollmentDate { get; set; }
public string Secret { get; set; }
}

Aunque no tenga un campo Secret en la página web, un hacker podría usar una herramienta como Fiddler, o
bien escribir código de JavaScript, para enviar un valor de formulario Secret . Sin el atributo Bind para limitar
los campos que el enlazador de modelos usa cuando crea una instancia Student, el enlazador de modelos
seleccionaría ese valor de formulario Secret y lo usaría para crear la instancia de la entidad Student. Después, el
valor que el hacker haya especificado para el campo de formulario Secret se actualizaría en la base de datos. En
la imagen siguiente se muestra cómo la herramienta Fiddler agrega el campo Secret (con el valor "OverPost") a
los valores de formulario enviados.
Después, el valor "OverPost" se agregaría correctamente a la propiedad Secret de la fila insertada, aunque no
hubiera previsto que la página web pudiera establecer esa propiedad.
Puede evitar la publicación excesiva en escenarios de edición si primero lee la entidad desde la base de datos y
después llama a TryUpdateModel , pasando una lista de propiedades permitidas de manera explícita. Es el método
que se usa en estos tutoriales.
Una manera alternativa de evitar la publicación excesiva que muchos desarrolladores prefieren consiste en usar
modelos de vista en lugar de clases de entidad con el enlace de modelos. Incluya en el modelo de vista solo las
propiedades que quiera actualizar. Una vez que haya finalizado el enlazador de modelos de MVC, copie las
propiedades del modelo de vista a la instancia de entidad, opcionalmente con una herramienta como
AutoMapper. Use _context.Entry en la instancia de entidad para establecer su estado en Unchanged y, después,
establezca Property("PropertyName").IsModified en true en todas las propiedades de entidad que se incluyan en
el modelo de vista. Este método funciona tanto en escenarios de edición como de creación.
Probar la página Create
En el código de Views/Students/Create.cshtml se usan las aplicaciones auxiliares de etiquetas label , input y
span (para los mensajes de validación) en cada campo.

Ejecute la aplicación, haga clic en la pestaña Students y después en Create New.


Escriba los nombres y una fecha. Pruebe a escribir una fecha no válida si el explorador se lo permite. (Algunos
exploradores le obligan a usar un selector de fecha). Después, haga clic en Crear para ver el mensaje de error.
Es la validación del lado servidor que obtendrá de forma predeterminada; en un tutorial posterior verá cómo
agregar atributos que también generan código para la validación del lado cliente. En el siguiente código resaltado
se muestra la comprobación de validación del modelo en el método Create .

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(
[Bind("EnrollmentDate,FirstMidName,LastName")] Student student)
{
try
{
if (ModelState.IsValid)
{
_context.Add(student);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
}
catch (DbUpdateException /* ex */)
{
//Log the error (uncomment ex variable name and write a log.
ModelState.AddModelError("", "Unable to save changes. " +
"Try again, and if the problem persists " +
"see your system administrator.");
}
return View(student);
}

Cambie la fecha por un valor válido y haga clic en Crear para ver el alumno nuevo en la página Index.

Actualizar la página Edit


En StudentController.cs, el método HttpGet Edit (el que no tiene el atributo HttpPost ) usa el método
SingleOrDefaultAsync para recuperar la entidad Student seleccionada, como se vio en el método Details . No es
necesario cambiar este método.
Código recomendado para HttpPost Edit: lectura y actualización
Reemplace el método de acción HttpPost Edit con el código siguiente.

[HttpPost, ActionName("Edit")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> EditPost(int? id)
{
if (id == null)
{
return NotFound();
}
var studentToUpdate = await _context.Students.SingleOrDefaultAsync(s => s.ID == id);
if (await TryUpdateModelAsync<Student>(
studentToUpdate,
"",
s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
{
try
{
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
catch (DbUpdateException /* ex */)
{
//Log the error (uncomment ex variable name and write a log.)
ModelState.AddModelError("", "Unable to save changes. " +
"Try again, and if the problem persists, " +
"see your system administrator.");
}
}
return View(studentToUpdate);
}

Estos cambios implementan un procedimiento recomendado de seguridad para evitar la publicación excesiva. El
proveedor de scaffolding generó un atributo Bind y agregó la entidad creada por el enlazador de modelos a la
entidad establecida con una marca Modified . Ese código no se recomienda para muchos escenarios porque el
atributo Bind borra los datos ya existentes en los campos que no se enumeran en el parámetro Include .
El código nuevo lee la entidad existente y llama a TryUpdateModel para actualizar los campos en la entidad
recuperada en función de la entrada del usuario en los datos de formulario publicados. El seguimiento de
cambios automático de Entity Framework establece la marca Modified en los campos que se cambian mediante
la entrada de formulario. Cuando se llama al método SaveChanges , Entity Framework crea instrucciones SQL
para actualizar la fila de la base de datos. Los conflictos de simultaneidad se ignoran y las columnas de tabla que
se actualizaron por el usuario se actualizan en la base de datos. (En un tutorial posterior se muestra cómo
controlar los conflictos de simultaneidad).
Como procedimiento recomendado para evitar la publicación excesiva, los campos que quiera que se puedan
actualizar por la página Edit se incluyen en la lista de permitidos en los parámetros TryUpdateModel . (La cadena
vacía que precede a la lista de campos en la lista de parámetros es para el prefijo que se usa con los nombres de
campos de formulario). Actualmente no se está protegiendo ningún campo adicional, pero enumerar los campos
que quiere que el enlazador de modelos enlace garantiza que, si en el futuro agrega campos al modelo de datos,
se protejan automáticamente hasta que los agregue aquí de forma explícita.
Como resultado de estos cambios, la firma de método del método HttpPost Edit es la misma que la del método
HttpGet Edit ; por tanto, se ha cambiado el nombre del método EditPost .
Código alternativo para HttpPost Edit: crear y adjuntar
El código recomendado para HttpPost Edit garantiza que solo se actualicen las columnas cambiadas y conserva
los datos de las propiedades que no quiere que se incluyan para el enlace de modelos. Pero el enfoque de
primera lectura requiere una operación de lectura adicional de la base de datos y puede dar lugar a código más
complejo para controlar los conflictos de simultaneidad. Una alternativa consiste en adjuntar una entidad creada
por el enlazador de modelos en el contexto de EF y marcarla como modificada. (No actualice el proyecto con este
código, solo se muestra para ilustrar un enfoque opcional).

public async Task<IActionResult> Edit(int id, [Bind("ID,EnrollmentDate,FirstMidName,LastName")] Student


student)
{
if (id != student.ID)
{
return NotFound();
}
if (ModelState.IsValid)
{
try
{
_context.Update(student);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
catch (DbUpdateException /* ex */)
{
//Log the error (uncomment ex variable name and write a log.)
ModelState.AddModelError("", "Unable to save changes. " +
"Try again, and if the problem persists, " +
"see your system administrator.");
}
}
return View(student);
}

Puede usar este enfoque cuando la interfaz de usuario de la página web incluya todos los campos de la entidad y
puede actualizar cualquiera de ellos.
En el código con scaffolding se usa el enfoque de crear y adjuntar, pero solo se detectan las excepciones
DbUpdateConcurrencyException y se devuelven códigos de error 404. En el ejemplo mostrado se detecta cualquier
excepción de actualización de base de datos y se muestra un mensaje de error.
Estados de entidad
El contexto de la base de datos realiza el seguimiento de si las entidades en memoria están sincronizadas con sus
filas correspondientes en la base de datos, y esta información determina lo que ocurre cuando se llama al método
SaveChanges . Por ejemplo, cuando se pasa una nueva entidad al método Add , el estado de esa entidad se
establece en Added . Después, cuando se llama al método SaveChanges , el contexto de la base de datos emite un
comando INSERT de SQL.
Una entidad puede estar en uno de los estados siguientes:
. La entidad no existe todavía en la base de datos. El método
Added SaveChanges emite una instrucción
INSERT.
Unchanged. No es necesario hacer nada con esta entidad mediante el método SaveChanges . Al leer una
entidad de la base de datos, la entidad empieza con este estado.
Modified . Se han modificado algunos o todos los valores de propiedad de la entidad. El método
SaveChanges emite una instrucción UPDATE.

Deleted . La entidad se ha marcado para su eliminación. El método SaveChanges emite una instrucción
DELETE.
Detached . El contexto de base de datos no está realizando el seguimiento de la entidad.
En una aplicación de escritorio, los cambios de estado normalmente se establecen de forma automática. Lea una
entidad y realice cambios en algunos de sus valores de propiedad. Esto hace que su estado de entidad cambie
automáticamente a Modified . Después, cuando se llama a SaveChanges , Entity Framework genera una
instrucción UPDATE de SQL que solo actualiza las propiedades reales que se hayan cambiado.
En una aplicación web, el DbContext que inicialmente lee una entidad y muestra sus datos para que se puedan
modificar se elimina después de representar una página. Cuando se llama al método de acción HttpPost Edit , se
realiza una nueva solicitud web y se obtiene una nueva instancia de DbContext . Si vuelve a leer la entidad en ese
contexto nuevo, simulará el procesamiento de escritorio.
Pero si no quiere realizar la operación de lectura adicional, tendrá que usar el objeto de entidad creado por el
enlazador de modelos. La manera más sencilla de hacerlo consiste en establecer el estado de la entidad en
Modified tal y como se hace en el código HttpPost Edit alternativo mostrado anteriormente. Después, cuando se
llama a SaveChanges , Entity Framework actualiza todas las columnas de la fila de la base de datos, porque el
contexto no tiene ninguna manera de saber qué propiedades se han cambiado.
Si quiere evitar el enfoque de primera lectura pero también que la instrucción UPDATE de SQL actualice solo los
campos que el usuario ha cambiado realmente, el código es más complejo. Debe guardar los valores originales
de alguna manera (por ejemplo mediante campos ocultos) para que estén disponibles cuando se llame al método
HttpPost Edit . Después puede crear una entidad Student con los valores originales, llamar al método Attach
con esa versión original de la entidad, actualizar los valores de la entidad con los valores nuevos y luego llamar a
SaveChanges .

Probar la página Edit


Ejecute la aplicación, haga clic en la pestaña Students y después en un hipervínculo Edit.

Cambie algunos de los datos y haga clic en Guardar. Se abrirá la página Index y verá los datos modificados.

Actualizar la página Delete


En StudentController.cs, el código de plantilla para el método HttpGet Delete usa el método
SingleOrDefaultAsync para recuperar la entidad Student seleccionada, como se vio en los métodos Details y Edit.
Pero para implementar un mensaje de error personalizado cuando se produce un error en la llamada a
SaveChanges , agregará funcionalidad a este método y su vista correspondiente.

Como se vio para las operaciones de actualización y creación, las operaciones de eliminación requieren dos
métodos de acción. El método que se llama en respuesta a una solicitud GET muestra una vista que proporciona
al usuario la oportunidad de aprobar o cancelar la operación de eliminación. Si el usuario la aprueba, se crea una
solicitud POST. Cuando esto ocurre, se llama al método HttpPost Delete y, después, ese método es el que realiza
la operación de eliminación.
Agregará un bloque try-catch al método HttpPost Delete para controlar los errores que puedan producirse
cuando se actualice la base de datos. Si se produce un error, el método HttpPost Delete llama al método HttpGet
Delete, pasando un parámetro que indica que se ha producido un error. Después, el método HttpGet Delete
vuelve a mostrar la página de confirmación junto con el mensaje de error, dando al usuario la oportunidad de
cancelar o volver a intentarlo.
Reemplace el método de acción HttpGet Delete con el código siguiente, que administra los informes de errores.

public async Task<IActionResult> Delete(int? id, bool? saveChangesError = false)


{
if (id == null)
{
return NotFound();
}

var student = await _context.Students


.AsNoTracking()
.SingleOrDefaultAsync(m => m.ID == id);
if (student == null)
{
return NotFound();
}

if (saveChangesError.GetValueOrDefault())
{
ViewData["ErrorMessage"] =
"Delete failed. Try again, and if the problem persists " +
"see your system administrator.";
}

return View(student);
}

Este código acepta un parámetro opcional que indica si se llamó al método después de un error al guardar los
cambios. Este parámetro es false cuando se llama al método HttpGet Delete sin un error anterior. Cuando se
llama por medio del método HttpPost Delete en respuesta a un error de actualización de base de datos, el
parámetro es true y se pasa un mensaje de error a la vista.
El enfoque de primera lectura para HttpPost Delete
Reemplace el método de acción HttpPost Delete (denominado DeleteConfirmed ) con el código siguiente, que
realiza la operación de eliminación y captura los errores de actualización de base de datos.
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
var student = await _context.Students
.AsNoTracking()
.SingleOrDefaultAsync(m => m.ID == id);
if (student == null)
{
return RedirectToAction(nameof(Index));
}

try
{
_context.Students.Remove(student);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
catch (DbUpdateException /* ex */)
{
//Log the error (uncomment ex variable name and write a log.)
return RedirectToAction(nameof(Delete), new { id = id, saveChangesError = true });
}
}

Este código recupera la entidad seleccionada y después llama al método Remove para establecer el estado de la
entidad en Deleted . Cuando se llama a SaveChanges , se genera un comando DELETE de SQL.
El enfoque de crear y adjuntar para HttpPost Delete
Si mejorar el rendimiento de una aplicación de gran volumen es una prioridad, podría evitar una consulta SQL
innecesaria creando instancias de una entidad Student solo con el valor de clave principal y después
estableciendo el estado de la entidad en Deleted . Eso es todo lo que necesita Entity Framework para eliminar la
entidad. (No incluya este código en el proyecto; únicamente se muestra para ilustrar una alternativa).

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
try
{
Student studentToDelete = new Student() { ID = id };
_context.Entry(studentToDelete).State = EntityState.Deleted;
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
catch (DbUpdateException /* ex */)
{
//Log the error (uncomment ex variable name and write a log.)
return RedirectToAction(nameof(Delete), new { id = id, saveChangesError = true });
}
}

Si la entidad tiene datos relacionados que también se deban eliminar, asegúrese de configurar la eliminación en
cascada en la base de datos. Con este enfoque de eliminación de entidades, es posible que EF no sepa que hay
entidades relacionadas para eliminar.
Actualizar la vista Delete
En Views/Student/Delete.cshtml, agregue un mensaje de error entre los títulos h2 y h3, como se muestra en el
ejemplo siguiente:
<h2>Delete</h2>
<p class="text-danger">@ViewData["ErrorMessage"]</p>
<h3>Are you sure you want to delete this?</h3>

Ejecute la aplicación, haga clic en la pestaña Students y después en un hipervínculo Delete:

Haga clic en Eliminar. Se mostrará la página de índice sin el estudiante eliminado. (Verá un ejemplo del código
de control de errores en funcionamiento en el tutorial sobre la simultaneidad).

Cierre de conexiones de bases de datos


Para liberar los recursos que contiene una conexión de base de datos, la instancia de contexto debe eliminarse tan
pronto como sea posible cuando haya terminado con ella. La inserción de dependencias integrada de ASP.NET
Core se encarga de esa tarea.
En Startup.cs, se llama al método de extensión AddDbContext para aprovisionar la clase DbContext en el
contenedor de inserción de dependencias de ASP.NET. Ese método establece la duración del servicio en Scoped
de forma predeterminada. Scoped significa que la duración del objeto de contexto coincide con la duración de la
solicitud web, y el método Dispose se llamará automáticamente al final de la solicitud web.

Control de transacciones
De forma predeterminada, Entity Framework implementa las transacciones de manera implícita. En escenarios
donde se realizan cambios en varias filas o tablas, y después se llama a SaveChanges , Entity Framework se
asegura automáticamente de que todos los cambios se realicen correctamente o se produzca un error en todos
ellos. Si primero se realizan algunos cambios y después se produce un error, los cambios se revierten
automáticamente. Para escenarios donde se necesita más control, por ejemplo, si se quieren incluir operaciones
realizadas fuera de Entity Framework en una transacción, vea Transacciones.

Consultas de no seguimiento
Cuando un contexto de base de datos recupera las filas de tabla y crea objetos de entidad que las representa, de
forma predeterminada realiza el seguimiento de si las entidades en memoria están sincronizadas con el
contenido de la base de datos. Los datos en memoria actúan como una caché y se usan cuando se actualiza una
entidad. Este almacenamiento en caché suele ser necesario en una aplicación web porque las instancias de
contexto normalmente son de corta duración (para cada solicitud se crea una y se elimina) y el contexto que lee
una entidad normalmente se elimina antes de volver a usar esa entidad.
Puede deshabilitar el seguimiento de los objetos de entidad en memoria mediante una llamada al método
AsNoTracking . Los siguientes son escenarios típicos en los que es posible que quiera hacer esto:

Durante la vigencia del contexto no es necesario actualizar ninguna entidad ni que EF cargue
automáticamente las propiedades de navegación con las entidades recuperadas por consultas
independientes. Estas condiciones se cumplen frecuentemente en los métodos de acción HttpGet del
controlador.
Se ejecuta una consulta que recupera un gran volumen de datos y solo se actualiza una pequeña parte de
los datos devueltos. Puede ser más eficaz desactivar el seguimiento de la consulta de gran tamaño y
ejecutar una consulta más adelante para las pocas entidades que deban actualizarse.
Se quiere adjuntar una entidad para actualizarla, pero antes se recuperó la misma entidad para un
propósito diferente. Como el contexto de base de datos ya está realizando el seguimiento de la entidad, no
se puede adjuntar la entidad que se quiere cambiar. Una manera de controlar esta situación consiste en
llamar a AsNoTracking en la consulta anterior.
Para obtener más información, vea Tracking vs. No-Tracking (Diferencia entre consultas de seguimiento y no
seguimiento).

Resumen
Ahora tiene un conjunto completo de páginas que realizan sencillas operaciones CRUD para entidades Student.
En el siguiente tutorial podrá expandir la funcionalidad de la página Index mediante la adición de ordenación,
filtrado y paginación.

A N T E R IO R S IG U IE N T E
ASP.NET Core MVC con EF Core: ordenación,
filtrado y paginación (3 de 10)
25/06/2018 • 26 minutes to read • Edit Online

Por Tom Dykstra y Rick Anderson


En la aplicación web de ejemplo Contoso University se muestra cómo crear aplicaciones web de ASP.NET Core
MVC con Entity Framework Core y Visual Studio. Para obtener información sobre la serie de tutoriales, consulte
el primer tutorial de la serie.
En el tutorial anterior, implementamos un conjunto de páginas web para operaciones básicas de CRUD para las
entidades Student. En este tutorial agregaremos la funcionalidad de ordenación, filtrado y paginación a la página
de índice de Students. También crearemos una página que realice agrupaciones sencillas.
En la siguiente ilustración se muestra el aspecto que tendrá la página cuando haya terminado. Los encabezados
de columna son vínculos en los que el usuario puede hacer clic para ordenar las columnas correspondientes. Si
se hace clic de forma consecutiva en el encabezado de una columna, el criterio de ordenación alterna entre
ascendente y descendente.

Agregar vínculos de ordenación de columnas en la página de índice de


Students
Para agregar ordenación a la página de índice de Student, deberá cambiar el método Index del controlador de
Students y agregar código a la vista de índice de Student.
Agregar la funcionalidad de ordenación al método Index
En StudentsController.cs, reemplace el método Index por el código siguiente:
public async Task<IActionResult> Index(string sortOrder)
{
ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
var students = from s in _context.Students
select s;
switch (sortOrder)
{
case "name_desc":
students = students.OrderByDescending(s => s.LastName);
break;
case "Date":
students = students.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
students = students.OrderByDescending(s => s.EnrollmentDate);
break;
default:
students = students.OrderBy(s => s.LastName);
break;
}
return View(await students.AsNoTracking().ToListAsync());
}

Este código recibe un parámetro sortOrder de la cadena de consulta en la dirección URL. ASP.NET Core MVC
proporciona el valor de la cadena de consulta como un parámetro al método de acción. El parámetro es una
cadena que puede ser "Name" o "Date", seguido (opcionalmente) por un guión bajo y la cadena "desc" para
especificar el orden descendente. El criterio de ordenación predeterminado es el ascendente.
La primera vez que se solicita la página de índice, no hay ninguna cadena de consulta. Los alumnos se muestran
por apellido en orden ascendente, que es el valor predeterminado establecido por el caso desestimado en la
instrucción switch . Cuando el usuario hace clic en un hipervínculo de encabezado de columna, se proporciona el
valor sortOrder correspondiente en la cadena de consulta.
La vista usa los dos elementos ViewData (NameSortParm y DateSortParm) para configurar los hipervínculos del
encabezado de columna con los valores de cadena de consulta adecuados.

public async Task<IActionResult> Index(string sortOrder)


{
ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
var students = from s in _context.Students
select s;
switch (sortOrder)
{
case "name_desc":
students = students.OrderByDescending(s => s.LastName);
break;
case "Date":
students = students.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
students = students.OrderByDescending(s => s.EnrollmentDate);
break;
default:
students = students.OrderBy(s => s.LastName);
break;
}
return View(await students.AsNoTracking().ToListAsync());
}

Estas son las instrucciones ternarias. La primera de ellas especifica que, si el parámetro sortOrder es NULL o
está vacío, NameSortParm debe establecerse en "name_desc"; en caso contrario, se debe establecer en una
cadena vacía. Estas dos instrucciones habilitan la vista para establecer los hipervínculos de encabezado de
columna de la forma siguiente:

CRITERIO DE ORDENACIÓN ACTUAL HIPERVÍNCULO DE APELLIDO HIPERVÍNCULO DE FECHA

Apellido: ascendente descending ascending

Apellido: descendente ascending ascending

Fecha: ascendente ascending descending

Fecha: descendente ascending ascending

El método usa LINQ to Entities para especificar la columna por la que se va a ordenar. El código crea una variable
IQueryable antes de la instrucción de cambio, la modifica en la instrucción de cambio y llama al método
ToListAsync después de la instrucción switch . Al crear y modificar variables IQueryable , no se envía ninguna
consulta a la base de datos. La consulta no se ejecuta hasta que convierta el objeto IQueryable en una colección
mediante una llamada a un método, como ToListAsync . Por lo tanto, este código produce una única consulta que
no se ejecuta hasta la instrucción return View .
Este código podría detallarse con un gran número de columnas. En el último tutorial de esta serie se muestra
cómo escribir código que le permita pasar el nombre de la columna OrderBy en una variable de cadenas.
Agregar hipervínculos del encabezado de columna a la vista de índice de Student
Reemplace el código de Views/Students/Index.cshtml por el código siguiente para agregar los hipervínculos del
encabezado de columna. Se resaltan las líneas modificadas.
@model IEnumerable<ContosoUniversity.Models.Student>

@{
ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
<a asp-action="Index" asp-route-
sortOrder="@ViewData["NameSortParm"]">@Html.DisplayNameFor(model => model.LastName)</a>
</th>
<th>
@Html.DisplayNameFor(model => model.FirstMidName)
</th>
<th>
<a asp-action="Index" asp-route-
sortOrder="@ViewData["DateSortParm"]">@Html.DisplayNameFor(model => model.EnrollmentDate)</a>
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.LastName)
</td>
<td>
@Html.DisplayFor(modelItem => item.FirstMidName)
</td>
<td>
@Html.DisplayFor(modelItem => item.EnrollmentDate)
</td>
<td>
<a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-action="Details" asp-route-id="@item.ID">Details</a> |
<a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>

Este código usa la información que se incluye en las propiedades ViewData para configurar hipervínculos con los
valores de cadena de consulta adecuados.
Ejecute la aplicación, seleccione la ficha Students y haga clic en los encabezados de columna Last Name y
Enrollment Date para comprobar que la ordenación funciona correctamente.
Agregar un cuadro de búsqueda a la página de índice de Students
Para agregar filtrado a la página de índice de Students, agregue un cuadro de texto y un botón de envío a la vista
y haga los cambios correspondientes en el método Index . El cuadro de texto le permite escribir la cadena que
quiera buscar en los campos de nombre y apellido.
Agregar la funcionalidad de filtrado al método Index
En StudentsController.cs, reemplace el método Index por el código siguiente (los cambios se resaltan).

public async Task<IActionResult> Index(string sortOrder, string searchString)


{
ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
ViewData["CurrentFilter"] = searchString;

var students = from s in _context.Students


select s;
if (!String.IsNullOrEmpty(searchString))
{
students = students.Where(s => s.LastName.Contains(searchString)
|| s.FirstMidName.Contains(searchString));
}
switch (sortOrder)
{
case "name_desc":
students = students.OrderByDescending(s => s.LastName);
break;
case "Date":
students = students.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
students = students.OrderByDescending(s => s.EnrollmentDate);
break;
default:
students = students.OrderBy(s => s.LastName);
break;
}
return View(await students.AsNoTracking().ToListAsync());
}
Ha agregado un parámetro searchString al método Index . El valor de la cadena de búsqueda se recibe desde
un cuadro de texto que agregará a la vista de índice. También ha agregado a la instrucción LINQ una cláusula
where que solo selecciona los alumnos cuyo nombre o apellido contienen la cadena de búsqueda. La instrucción
que agrega la cláusula where solo se ejecuta si hay un valor que se tiene que buscar.

NOTE
Aquí se llama al método Where en un objeto IQueryable y el filtro se procesa en el servidor. En algunos escenarios,
puede hacer una llamada al método Where como un método de extensión en una colección en memoria. (Por ejemplo,
imagine que cambia la referencia a _context.Students , de modo que, en lugar de hacer referencia a EF DbSet , haga
referencia a un método de repositorio que devuelva una colección IEnumerable ). Lo más habitual es que el resultado
fuera el mismo, pero en algunos casos puede ser diferente.
Por ejemplo, la implementación de .NET Framework del método Contains realiza de forma predeterminada una
comparación que diferencia entre mayúsculas y minúsculas, pero en SQL Server se determina por la configuración de
intercalación de la instancia de SQL Server. De forma predeterminada, esta opción de configuración no diferencia entre
mayúsculas y minúsculas. Podría hacer una llamada al método ToUpper para hacer explícitamente que la prueba no
diferenciara entre mayúsculas y minúsculas: donde (s => s.LastName.ToUpper().Contains (searchString.ToUpper()). Esto
garantiza que los resultados permanezcan invariables aunque cambie el código más adelante para usar un repositorio que
devuelva una colección IEnumerable en vez de un objeto IQueryable . (Al hacer una llamada al método Contains en
una colección IEnumerable , obtendrá la implementación de .NET Framework; al hacer una llamada a un objeto
IQueryable , obtendrá la implementación del proveedor de base de datos). En cambio, el rendimiento de esta solución se
ve reducido. El código ToUpper tendría que poner una función en la cláusula WHERE de la instrucción SELECT de TSQL.
Esto impediría que el optimizador usara un índice. Dado que principalmente SQL se instala de forma que no diferencia entre
mayúsculas y minúsculas, es mejor que evite el código ToUpper hasta que migre a un almacén de datos que distinga
entre mayúsculas y minúsculas.

Agregar un cuadro de búsqueda a la vista de índice de Student


En Views/Student/Index.cshtml, agregue el código resaltado justo antes de la etiqueta de apertura de tabla para
crear un título, un cuadro de texto y un botón de búsqueda.

<p>
<a asp-action="Create">Create New</a>
</p>

<form asp-action="Index" method="get">


<div class="form-actions no-color">
<p>
Find by name: <input type="text" name="SearchString" value="@ViewData["currentFilter"]" />
<input type="submit" value="Search" class="btn btn-default" /> |
<a asp-action="Index">Back to Full List</a>
</p>
</div>
</form>

<table class="table">

Este código usa la aplicación auxiliar de etiquetas <form> para agregar el cuadro de texto de búsqueda y el
botón. De forma predeterminada, la aplicación auxiliar de etiquetas <form> envía datos de formulario con POST,
lo que significa que los parámetros se pasan en el cuerpo del mensaje HTTP y no en la dirección URL como
cadenas de consulta. Al especificar HTTP GET, los datos de formulario se pasan en la dirección URL como
cadenas de consulta, lo que permite que los usuarios marquen la dirección URL. Las directrices de W3C
recomiendan que use GET cuando la acción no produzca ninguna actualización.
Ejecute la aplicación, seleccione la ficha Students, escriba una cadena de búsqueda y haga clic en Search para
comprobar que el filtrado funciona correctamente.
Fíjese en que la dirección URL contiene la cadena de búsqueda.

http://localhost:5813/Students?SearchString=an

Si marca esta página, obtendrá la lista filtrada al usar el marcador. El hecho de agregar method="get" a la etiqueta
form es lo que ha provocado que se generara la cadena de consulta.

En esta fase, si hace clic en un vínculo de ordenación del encabezado de columna, el valor de filtro que especificó
en el cuadro de búsqueda se perderá. Podrá corregirlo en la siguiente sección.

Agregar la funcionalidad de paginación a la página de índice de


Students
Para agregar paginación a la página de índice de Students, tendrá que crear una clase PaginatedList que use las
instrucciones Skip y Take para filtrar los datos en el servidor en lugar de recuperar siempre todas las filas de la
tabla. A continuación, podrá realizar cambios adicionales en el método Index y agregar botones de paginación a
la vista Index . La ilustración siguiente muestra los botones de paginación.
En la carpeta del proyecto, cree PaginatedList.cs y después reemplace el código de plantilla por el código
siguiente.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity
{
public class PaginatedList<T> : List<T>
{
public int PageIndex { get; private set; }
public int TotalPages { get; private set; }

public PaginatedList(List<T> items, int count, int pageIndex, int pageSize)


{
PageIndex = pageIndex;
TotalPages = (int)Math.Ceiling(count / (double)pageSize);

this.AddRange(items);
}

public bool HasPreviousPage


{
get
{
return (PageIndex > 1);
}
}

public bool HasNextPage


{
get
{
return (PageIndex < TotalPages);
}
}

public static async Task<PaginatedList<T>> CreateAsync(IQueryable<T> source, int pageIndex, int


pageSize)
{
var count = await source.CountAsync();
var items = await source.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync();
return new PaginatedList<T>(items, count, pageIndex, pageSize);
}
}
}

El método CreateAsync en este código toma el tamaño y el número de la página y aplica las instrucciones Skip
y Take correspondientes a IQueryable . Cuando se llama a ToListAsync en IQueryable , devuelve una lista que
solo contiene la página solicitada. Las propiedades HasPreviousPage y HasNextPage se pueden usar para habilitar
o deshabilitar los botones de página Previous y Next.
Para crear el objeto PaginatedList<T> , se usa un método CreateAsync en vez de un constructor, porque los
constructores no pueden ejecutar código asincrónico.

Agregar la funcionalidad de paginación al método Index


En StudentsController.cs, reemplace el método Index por el código siguiente.
public async Task<IActionResult> Index(
string sortOrder,
string currentFilter,
string searchString,
int? page)
{
ViewData["CurrentSort"] = sortOrder;
ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";

if (searchString != null)
{
page = 1;
}
else
{
searchString = currentFilter;
}

ViewData["CurrentFilter"] = searchString;

var students = from s in _context.Students


select s;
if (!String.IsNullOrEmpty(searchString))
{
students = students.Where(s => s.LastName.Contains(searchString)
|| s.FirstMidName.Contains(searchString));
}
switch (sortOrder)
{
case "name_desc":
students = students.OrderByDescending(s => s.LastName);
break;
case "Date":
students = students.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
students = students.OrderByDescending(s => s.EnrollmentDate);
break;
default:
students = students.OrderBy(s => s.LastName);
break;
}

int pageSize = 3;
return View(await PaginatedList<Student>.CreateAsync(students.AsNoTracking(), page ?? 1, pageSize));
}

Este código agrega un parámetro de número de página, un parámetro de criterio de ordenación actual y un
parámetro de filtro actual a la firma del método.

public async Task<IActionResult> Index(


string sortOrder,
string currentFilter,
string searchString,
int? page)

La primera vez que se muestra la página, o si el usuario no ha hecho clic en un vínculo de ordenación o
paginación, todos los parámetros son nulos. Si se hace clic en un vínculo de paginación, la variable de página
contiene el número de página que se tiene que mostrar.
El elemento ViewData , denominado CurrentSort, proporciona la vista con el criterio de ordenación actual, que
debe incluirse en los vínculos de paginación para mantener el criterio de ordenación durante la paginación.
El elemento ViewData , denominado CurrentFilter, proporciona la vista con la cadena de filtro actual. Este valor
debe incluirse en los vínculos de paginación para mantener la configuración de filtrado durante la paginación y
debe restaurarse en el cuadro de texto cuando se vuelve a mostrar la página.
Si se cambia la cadena de búsqueda durante la paginación, la página debe restablecerse a 1, porque el nuevo
filtro puede hacer que se muestren diferentes datos. La cadena de búsqueda cambia cuando se escribe un valor
en el cuadro de texto y se presiona el botón Submit. En ese caso, el parámetro searchString no es NULL.

if (searchString != null)
{
page = 1;
}
else
{
searchString = currentFilter;
}

Al final del método Index , el método PaginatedList.CreateAsync convierte la consulta del alumno en una sola
página de alumnos de un tipo de colección que admita la paginación. Entonces, esa única página de alumnos
pasa a la vista.

return View(await PaginatedList<Student>.CreateAsync(students.AsNoTracking(), page ?? 1, pageSize));

El método PaginatedList.CreateAsync toma un número de página. Los dos signos de interrogación representan
el operador de uso combinado de NULL. El operador de uso combinado de NULL define un valor
predeterminado para un tipo que acepta valores NULL; la expresión (page ?? 1) devuelve el valor de page si
tiene algún valor o devuelve 1 si page es NULL.

Agregar vínculos de paginación a la vista de índice de Student


En Views/Students/Index.cshtml, reemplace el código existente por el código siguiente. Los cambios aparecen
resaltados.

@model PaginatedList<ContosoUniversity.Models.Student>

@{
ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
<a asp-action="Create">Create New</a>
</p>

<form asp-action="Index" method="get">


<div class="form-actions no-color">
<p>
Find by name: <input type="text" name="SearchString" value="@ViewData["currentFilter"]" />
<input type="submit" value="Search" class="btn btn-default" /> |
<a asp-action="Index">Back to Full List</a>
</p>
</div>
</form>

<table class="table">
<thead>
<tr>
<th>
<a asp-action="Index" asp-route-sortOrder="@ViewData["NameSortParm"]" asp-route-
<a asp-action="Index" asp-route-sortOrder="@ViewData["NameSortParm"]" asp-route-
currentFilter="@ViewData["CurrentFilter"]">Last Name</a>
</th>
<th>
First Name
</th>
<th>
<a asp-action="Index" asp-route-sortOrder="@ViewData["DateSortParm"]" asp-route-
currentFilter="@ViewData["CurrentFilter"]">Enrollment Date</a>
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.LastName)
</td>
<td>
@Html.DisplayFor(modelItem => item.FirstMidName)
</td>
<td>
@Html.DisplayFor(modelItem => item.EnrollmentDate)
</td>
<td>
<a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-action="Details" asp-route-id="@item.ID">Details</a> |
<a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>

@{
var prevDisabled = !Model.HasPreviousPage ? "disabled" : "";
var nextDisabled = !Model.HasNextPage ? "disabled" : "";
}

<a asp-action="Index"
asp-route-sortOrder="@ViewData["CurrentSort"]"
asp-route-page="@(Model.PageIndex - 1)"
asp-route-currentFilter="@ViewData["CurrentFilter"]"
class="btn btn-default @prevDisabled">
Previous
</a>
<a asp-action="Index"
asp-route-sortOrder="@ViewData["CurrentSort"]"
asp-route-page="@(Model.PageIndex + 1)"
asp-route-currentFilter="@ViewData["CurrentFilter"]"
class="btn btn-default @nextDisabled">
Next
</a>

La instrucción @model de la parte superior de la página especifica que ahora la vista obtiene un objeto
PaginatedList<T> en lugar de un objeto List<T> .
Los vínculos del encabezado de la columna usan la cadena de consulta para pasar la cadena de búsqueda actual
al controlador, de modo que el usuario pueda ordenar los resultados del filtro:

<a asp-action="Index" asp-route-sortOrder="@ViewData["DateSortParm"]" asp-route-currentFilter


="@ViewData["CurrentFilter"]">Enrollment Date</a>

Los botones de paginación se muestran mediante aplicaciones auxiliares de etiquetas:


<a asp-action="Index"
asp-route-sortOrder="@ViewData["CurrentSort"]"
asp-route-page="@(Model.PageIndex - 1)"
asp-route-currentFilter="@ViewData["CurrentFilter"]"
class="btn btn-default @prevDisabled">
Previous
</a>

Ejecute la aplicación y vaya a la página Students.

Haga clic en los vínculos de paginación en distintos criterios de ordenación para comprobar que la paginación
funciona correctamente. A continuación, escriba una cadena de búsqueda e intente llevar a cabo la paginación de
nuevo, para comprobar que la paginación también funciona correctamente con filtrado y ordenación.

Creación de una página About que muestra las estadísticas de los


alumnos
En la página About del sitio web de Contoso University, se muestran cuántos alumnos se han inscrito en cada
fecha de inscripción. Esto requiere realizar agrupaciones y cálculos sencillos en los grupos. Para conseguirlo, haga
lo siguiente:
Cree una clase de modelo de vista para los datos que necesita pasar a la vista.
Modifique el método About en el controlador Home.
Modifique la vista About.
Creación del modelo de vista
Cree una carpeta SchoolViewModels en la carpeta Models.
En la nueva carpeta, agregue un archivo de clase EnrollmentDateGroup.cs y reemplace el código de plantilla con
el código siguiente:
using System;
using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models.SchoolViewModels
{
public class EnrollmentDateGroup
{
[DataType(DataType.Date)]
public DateTime? EnrollmentDate { get; set; }

public int StudentCount { get; set; }


}
}

Modificación del controlador Home


En HomeController.cs, agregue lo siguiente mediante instrucciones en la parte superior del archivo:

using Microsoft.EntityFrameworkCore;
using ContosoUniversity.Data;
using ContosoUniversity.Models.SchoolViewModels;

Agregue una variable de clase para el contexto de base de datos inmediatamente después de la llave de apertura
para la clase y obtenga una instancia del contexto de ASP.NET Core DI:

public class HomeController : Controller


{
private readonly SchoolContext _context;

public HomeController(SchoolContext context)


{
_context = context;
}

Reemplace el método About con el código siguiente:

public async Task<ActionResult> About()


{
IQueryable<EnrollmentDateGroup> data =
from student in _context.Students
group student by student.EnrollmentDate into dateGroup
select new EnrollmentDateGroup()
{
EnrollmentDate = dateGroup.Key,
StudentCount = dateGroup.Count()
};
return View(await data.AsNoTracking().ToListAsync());
}

La instrucción LINQ agrupa las entidades de alumnos por fecha de inscripción, calcula la cantidad de entidades
que se incluyen en cada grupo y almacena los resultados en una colección de objetos de modelo de la vista
EnrollmentDateGroup .
NOTE
En la versión 1.0 de Entity Framework Core, el conjunto de resultados completo se devuelve al cliente y la agrupación se
realiza en el cliente. En algunos casos, esto puede crear problemas de rendimiento. Asegúrese de probar el rendimiento con
volúmenes de producción de datos y, si es necesario, use SQL sin formato para realizar la agrupación en el servidor. Para
obtener información sobre cómo usar SQL sin formato, consulte el último tutorial de esta serie.

Modificación de la vista About


Reemplace el código del archivo Views/Home/About.cshtml por el código siguiente:

@model IEnumerable<ContosoUniversity.Models.SchoolViewModels.EnrollmentDateGroup>

@{
ViewData["Title"] = "Student Body Statistics";
}

<h2>Student Body Statistics</h2>

<table>
<tr>
<th>
Enrollment Date
</th>
<th>
Students
</th>
</tr>

@foreach (var item in Model)


{
<tr>
<td>
@Html.DisplayFor(modelItem => item.EnrollmentDate)
</td>
<td>
@item.StudentCount
</td>
</tr>
}
</table>

Ejecute la aplicación y vaya a la página About. En una tabla se muestra el número de alumnos para cada fecha de
inscripción.
Resumen
En este tutorial, ha visto cómo realizar la ordenación, el filtrado, la paginación y la agrupación. En el siguiente
tutorial, aprenderá a controlar los cambios en el modelo de datos mediante migraciones.

A N T E R IO R S IG U IE N T E
ASP.NET Core MVC con EF Core: Migraciones (4 de
10)
25/06/2018 • 14 minutes to read • Edit Online

Por Tom Dykstra y Rick Anderson


En la aplicación web de ejemplo Contoso University se muestra cómo crear aplicaciones web de ASP.NET Core
MVC con Entity Framework Core y Visual Studio. Para obtener información sobre la serie de tutoriales,
consulte el primer tutorial de la serie.
En este tutorial, empezará usando la característica de migraciones de EF Core para administrar cambios en el
modelo de datos. En los tutoriales posteriores, agregará más migraciones a medida que cambie el modelo de
datos.

Introducción a las migraciones


Al desarrollar una aplicación nueva, el modelo de datos cambia con frecuencia y, cada vez que lo hace, se deja
de sincronizar con la base de datos. Estos tutoriales se iniciaron con la configuración de Entity Framework para
crear la base de datos si no existía. Después, cada vez que cambie el modelo de datos (agregar, quitar o cambiar
las clases de entidad, o bien cambiar la clase DbContext), puede eliminar la base de datos y EF crea una que
coincida con el modelo y la inicializa con datos de prueba.
Este método para mantener la base de datos sincronizada con el modelo de datos funciona bien hasta que la
aplicación se implemente en producción. Cuando la aplicación se ejecuta en producción, normalmente
almacena los datos que le interesa mantener y no querrá perderlo todo cada vez que realice un cambio, como al
agregar una columna nueva. La característica Migraciones de EF Core soluciona este problema habilitando EF
para actualizar el esquema de la base de datos en lugar de crear una.

Paquetes de Entity Framework Core NuGet para migraciones


Para trabajar con las migraciones, puede usar la Consola del Administrador de paquetes (PMC ) o la interfaz
de la línea de comandos (CLI). En estos tutoriales se muestra cómo usar los comandos de la CLI. Al final de este
tutorial encontrará información sobre la PMC.
Las herramientas de EF para la interfaz de nivel de la línea de comandos se proporcionan en
Microsoft.EntityFrameworkCore.Tools.DotNet. Para instalar este paquete, agréguelo a la colección
DotNetCliToolReference del archivo .csproj, como se muestra a continuación. Nota: Tendrá que instalar este
paquete mediante la edición del archivo .csproj; no se puede usar el comando install-package ni la interfaz
gráfica de usuario del administrador de paquetes. Puede editar el archivo .csproj si hace clic con el botón
derecho en el nombre del proyecto en el Explorador de soluciones y selecciona Editar
ContosoUniversity.csproj.

<ItemGroup>
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.0" />
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.0" />
</ItemGroup>

(Los números de versión en este ejemplo eran los actuales cuando se escribió el tutorial).

Cambiar la cadena de conexión


En el archivo appsettings.json, cambie el nombre de la base de datos en la cadena de conexión por
ContosoUniversity2 u otro nombre que no haya usado en el equipo que esté usando.

{
"ConnectionStrings": {
"DefaultConnection": "Server=
(localdb)\\mssqllocaldb;Database=ContosoUniversity2;Trusted_Connection=True;MultipleActiveResultSets=true"
},

Este cambio configura el proyecto para que la primera migración cree una base de datos. Esto no es necesario
para comenzar a usar las migraciones, pero más adelante se verá por qué es una buena idea.

NOTE
Como alternativa a cambiar el nombre de la base de datos, puede eliminar la base de datos. Use el Explorador de
objetos de SQL Server (SSOX) o el comando de la CLI database drop :

dotnet ef database drop

En la siguiente sección se explica cómo ejecutar comandos de la CLI.

Crear una migración inicial


Guarde los cambios y compile el proyecto. Después, abra una ventana de comandos y desplácese hasta la
carpeta del proyecto. Esta es una forma rápida de hacerlo:
En el Explorador de soluciones, haga clic con el botón derecho en el proyecto y elija Abrir en el
Explorador de archivos en el menú contextual.

Escriba "cmd" en la barra de direcciones y presione Entrar.


Escriba el siguiente comando en la ventana de comandos:

dotnet ef migrations add InitialCreate

En la ventana de comandos verá un resultado similar al siguiente:

info: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[0]
User profile is available. Using 'C:\Users\username\AppData\Local\ASP.NET\DataProtection-Keys' as key
repository and Windows DPAPI to encrypt keys at rest.
info: Microsoft.EntityFrameworkCore.Infrastructure[100403]
Entity Framework Core 2.0.0-rtm-26452 initialized 'SchoolContext' using provider
'Microsoft.EntityFrameworkCore.SqlServer' with options: None
Done. To undo this action, use 'ef migrations remove'

NOTE
Si ve un mensaje de error No se encuentra ningún archivo ejecutable que coincida con el comando "dotnet-ef", vea esta
entrada de blog para obtener ayuda para solucionar problemas.

Si ve un mensaje de error "No se puede obtener acceso al archivo... ContosoUniversity.dll porque lo está
usando otro proceso.", busque el icono de IIS Express en la bandeja del sistema de Windows, haga clic con el
botón derecho en él y, después, haga clic en ContosoUniversity > Detener sitio.

Examinar los métodos Up y Down


Cuando ejecutó el comando migrations add , EF generó el código que va a crear la base de datos desde cero.
Este código está en la carpeta Migrations, en el archivo denominado <marca_de_tiempo>_InitialCreate.cs. El
método Up de la clase InitialCreate crea las tablas de base de datos que corresponden a los conjuntos de
entidades del modelo de datos y el método Down las elimina, como se muestra en el ejemplo siguiente.
public partial class InitialCreate : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Course",
columns: table => new
{
CourseID = table.Column<int>(nullable: false),
Credits = table.Column<int>(nullable: false),
Title = table.Column<string>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Course", x => x.CourseID);
});

// Additional code not shown


}

protected override void Down(MigrationBuilder migrationBuilder)


{
migrationBuilder.DropTable(
name: "Enrollment");
// Additional code not shown
}
}

Las migraciones llaman al método Up para implementar los cambios del modelo de datos para una migración.
Cuando se escribe un comando para revertir la actualización, las migraciones llaman al método Down .
Este código es para la migración inicial que se creó cuando se escribió el comando
migrations add InitialCreate . El parámetro de nombre de la migración ("InitialCreate" en el ejemplo) se usa
para el nombre de archivo y puede ser lo que quiera. Es más recomendable elegir una palabra o frase que
resuma lo que se hace en la migración. Por ejemplo, podría denominar "AddDepartmentTable" a una migración
posterior.
Si creó la migración inicial cuando la base de datos ya existía, se genera el código de creación de la base de
datos pero no es necesario ejecutarlo porque la base de datos ya coincide con el modelo de datos. Al
implementar la aplicación en otro entorno donde la base de datos todavía no existe, se ejecutará este código
para crear la base de datos, por lo que es recomendable probarlo primero. Por ese motivo se cambió antes el
nombre de la base de datos en la cadena de conexión, para que las migraciones puedan crear uno desde cero.

La instantánea del modelo de datos


Las migraciones crean una instantánea del esquema de la base de datos actual en
Migrations/SchoolContextModelSnapshot.cs. Cuando se agrega una migración, EF determina qué ha cambiado
mediante la comparación del modelo de datos con el archivo de instantánea.
Cuando elimine una migración, use el comando dotnet ef migrations remove. dotnet ef migrations remove
elimina la migración y garantiza que la instantánea se restablece correctamente.
Vea Migraciones en entornos de equipo para más información sobre cómo se usa el archivo de instantánea.

Aplicar la migración a la base de datos


En la ventana de comandos, escriba el comando siguiente para crear la base de datos y tablas en su interior.
dotnet ef database update

El resultado del comando es similar al comando migrations add , con la excepción de que verá registros para
los comandos SQL que configuran la base de datos. La mayoría de los registros se omite en la siguiente salida
de ejemplo. Si prefiere no ver este nivel de detalle en los mensajes de registro, puede cambiarlo en el archivo
appsettings.Development.json. Para obtener más información, vea Introducción al registro.

info: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[0]
User profile is available. Using 'C:\Users\username\AppData\Local\ASP.NET\DataProtection-Keys' as key
repository and Windows DPAPI to encrypt keys at rest.
info: Microsoft.EntityFrameworkCore.Infrastructure[100403]
Entity Framework Core 2.0.0-rtm-26452 initialized 'SchoolContext' using provider
'Microsoft.EntityFrameworkCore.SqlServer' with options: None
info: Microsoft.EntityFrameworkCore.Database.Command[200101]
Executed DbCommand (467ms) [Parameters=[], CommandType='Text', CommandTimeout='60']
CREATE DATABASE [ContosoUniversity2];
info: Microsoft.EntityFrameworkCore.Database.Command[200101]
Executed DbCommand (20ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
CREATE TABLE [__EFMigrationsHistory] (
[MigrationId] nvarchar(150) NOT NULL,
[ProductVersion] nvarchar(32) NOT NULL,
CONSTRAINT [PK___EFMigrationsHistory] PRIMARY KEY ([MigrationId])
);

<logs omitted for brevity>

info: Microsoft.EntityFrameworkCore.Database.Command[200101]
Executed DbCommand (3ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES (N'20170816151242_InitialCreate', N'2.0.0-rtm-26452');
Done.

Use el Explorador de objetos de SQL Server para inspeccionar la base de datos como hizo en el primer
tutorial. Observará la adición de una tabla __EFMigrationsHistory que realiza el seguimiento de las migraciones
que se han aplicado a la base de datos. Si examina los datos de esa tabla, verá una fila para la primera
migración. (En el último registro del ejemplo de salida de la CLI anterior se muestra la instrucción INSERT que
crea esta fila).
Ejecute la aplicación para comprobar que todo funciona igual que antes.
Diferencias entre la interfaz de la línea de comandos (CLI) y la
Consola del Administrador de paquetes (PMC)
Las herramientas de EF para la administración de migraciones están disponibles desde los comandos de la CLI
de .NET Core o los cmdlets de PowerShell en la ventana Consola del Administrador de paquetes (PMC ) de
Visual Studio. En este tutorial se muestra cómo usar la CLI, pero puede usar la PMC si lo prefiere.
Los comandos de EF para los comandos de la PMC están en el paquete Microsoft.EntityFrameworkCore.Tools.
Este paquete ya está incluido en el metapaquete Microsoft.AspNetCore.All, por lo que no es necesario
instalarlo.
Importante: Este no es el mismo paquete que el que se instala para la CLI mediante la edición del archivo
.csproj. El nombre de este paquete termina en Tools , a diferencia del nombre de paquete de la CLI que termina
en Tools.DotNet .
Para obtener más información sobre los comandos de la CLI, vea CLI de .NET Core.
Para obtener más información sobre los comandos de la PMC, vea Consola del Administrador de paquetes
(Visual Studio).

Resumen
En este tutorial, ha visto cómo crear y aplicar la primera migración. En el siguiente, comenzará examinando
temas más avanzados expandiendo el modelo de datos. Por el camino, podrá crear y aplicar migraciones
adicionales.

A N T E R IO R S IG U IE N T E
ASP.NET Core MVC con EF Core: Modelo de datos
(5 de 10)
25/06/2018 • 53 minutes to read • Edit Online

Por Tom Dykstra y Rick Anderson


En la aplicación web de ejemplo Contoso University se muestra cómo crear aplicaciones web de ASP.NET Core
MVC con Entity Framework Core y Visual Studio. Para obtener información sobre la serie de tutoriales, consulte
el primer tutorial de la serie.
En los tutoriales anteriores, trabajó con un modelo de datos simple que se componía de tres entidades. En este
tutorial agregará más entidades y relaciones, y personalizará el modelo de datos mediante la especificación de
reglas de formato, validación y asignación de base de datos.
Cuando haya terminado, las clases de entidad conformarán el modelo de datos completo que se muestra en la
ilustración siguiente:
Personalizar el modelo de datos mediante el uso de atributos
En esta sección verá cómo personalizar el modelo de datos mediante el uso de atributos que especifican reglas
de formato, validación y asignación de base de datos. Después, en varias de las secciones siguientes, creará el
modelo de datos School completo mediante la adición de atributos a las clases que ya ha creado y la creación de
clases para los demás tipos de entidad del modelo.
El atributo DataType
Para las fechas de inscripción de estudiantes, en todas las páginas web se muestra actualmente la hora junto con
la fecha, aunque todo lo que le interesa para este campo es la fecha. Mediante los atributos de anotación de
datos, puede realizar un cambio de código que fijará el formato de presentación en cada vista en la que se
muestren los datos. Para ver un ejemplo de cómo hacerlo, deberá agregar un atributo a la propiedad
EnrollmentDate en la clase Student .

En Models/Student.cs, agregue una instrucción using para el espacio de nombres


System.ComponentModel.DataAnnotations y los atributos DataType y DisplayFormat a la propiedad
EnrollmentDate , como se muestra en el ejemplo siguiente:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models
{
public class Student
{
public int ID { get; set; }
public string LastName { get; set; }
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime EnrollmentDate { get; set; }

public ICollection<Enrollment> Enrollments { get; set; }


}
}

El atributo DataType se usa para especificar un tipo de datos más específico que el tipo intrínseco de base de
datos. En este caso solo se quiere realizar el seguimiento de la fecha, no de la fecha y la hora. La enumeración
DataType proporciona muchos tipos de datos, como Date ( Fecha), Time ( Hora), PhoneNumber ( Número de
teléfono), Currency (Divisa), EmailAddress (Dirección de correo electrónico) y muchos más. El atributo DataType
también puede permitir que la aplicación proporcione automáticamente características específicas del tipo. Por
ejemplo, se puede crear un vínculo mailto: para DataType.EmailAddress y se puede proporcionar un selector de
datos para DataType.Date en exploradores compatibles con HTML5. El atributo DataType emite atributos
data- de HTML 5 (se pronuncia "datos dash") que los exploradores HTML 5 pueden comprender. Los atributos
DataType no proporcionan ninguna validación.

DataType.Date no especifica el formato de la fecha que se muestra. De manera predeterminada, el campo de


datos se muestra según los formatos predeterminados basados en el elemento CultureInfo del servidor.
El atributo DisplayFormat se usa para especificar el formato de fecha de forma explícita:

[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]

El valor ApplyFormatInEditMode especifica que el formato se debe aplicar también cuando el valor se muestra en
un cuadro de texto para su edición. (Es posible que no le interese ese comportamiento para algunos campos, por
ejemplo, para los valores de divisa, es posible que no quiera que el símbolo de la divisa se incluya en el cuadro
de texto editable).
Se puede usar el atributo DisplayFormat por sí solo, pero normalmente se recomienda usar también el atributo
DataType . El atributo DataType transmite la semántica de los datos en contraposición a cómo se representan en
una pantalla y ofrece las siguientes ventajas que no proporciona DisplayFormat :
El explorador puede habilitar características de HTML5 (por ejemplo, para mostrar un control de
calendario, el símbolo de divisa adecuado según la configuración regional, vínculos de correo electrónico,
validación de entradas del lado cliente, etc.).
De manera predeterminada, el explorador representa los datos con el formato correcto según la
configuración regional.
Para obtener más información, vea la documentación de la aplicación auxiliar de etiquetas <entrada>.
Ejecute la aplicación, vaya a la página Students Index y verá que ya no se muestran las horas para las fechas de
inscripción. Lo mismo sucede para cualquier vista en la que se use el modelo Student.

El atributo StringLength
También puede especificar reglas de validación de datos y mensajes de error de validación mediante atributos. El
atributo StringLength establece la longitud máxima de la base de datos y proporciona la validación del lado
cliente y el lado servidor para ASP.NET MVC. En este atributo también se puede especificar la longitud mínima
de la cadena, pero el valor mínimo no influye en el esquema de la base de datos.
Imagine que quiere asegurarse de que los usuarios no escriban más de 50 caracteres para un nombre. Para
agregar esta limitación, agregue atributos StringLength a las propiedades LastName y FirstMidName , como se
muestra en el ejemplo siguiente:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models
{
public class Student
{
public int ID { get; set; }
[StringLength(50)]
public string LastName { get; set; }
[StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime EnrollmentDate { get; set; }

public ICollection<Enrollment> Enrollments { get; set; }


}
}

El atributo StringLength no impedirá que un usuario escriba un espacio en blanco para un nombre. Puede usar
el atributo RegularExpression para aplicar restricciones a la entrada. Por ejemplo, el código siguiente requiere
que el primer carácter sea una letra mayúscula y el resto de caracteres sean alfabéticos:

[RegularExpression(@"^[A-Z]+[a-zA-Z""'\s-]*$")]

El atributo MaxLength proporciona una funcionalidad similar a la del atributo StringLength pero no proporciona
la validación del lado cliente.
Ahora el modelo de base de datos ha cambiado de tal forma que se requiere un cambio en el esquema de la
base de datos. Deberá usar migraciones para actualizar el esquema sin perder los datos que pueda haber
agregado a la base de datos mediante la interfaz de usuario de la aplicación.
Guarde los cambios y compile el proyecto. Después, abra la ventana de comandos en la carpeta de proyecto y
escriba los comandos siguientes:

dotnet ef migrations add MaxLengthOnNames

dotnet ef database update

El comando migrations add advierte de que se puede producir pérdida de datos, porque el cambio reduce la
longitud máxima para dos columnas. Las migraciones crean un archivo denominado
<marca_de_tiempo>_MaxLengthOnNames.cs. Este archivo contiene código en el método Up que actualizará la
base de datos para que coincida con el modelo de datos actual. El comando database update ejecutó ese código.
Entity Framework usa la marca de tiempo que precede al nombre de archivo de migraciones para ordenar las
migraciones. Puede crear varias migraciones antes de ejecutar el comando de actualización de bases de datos y,
después, todas las migraciones se aplican en el orden en el que se hayan creado.
Ejecute la aplicación, haga clic en la pestaña Students, haga clic en Create New (Crear) y escriba cualquier
nombre de más de 50 caracteres. Al hacer clic en Create (Crear), la validación del lado cliente muestra un
mensaje de error.
El atributo Column
También puede usar atributos para controlar cómo se asignan las clases y propiedades a la base de datos.
Imagine que hubiera usado el nombre FirstMidName para el nombre de campo por la posibilidad de que el
campo contenga también un segundo nombre. Pero quiere que la columna de base de datos se denomine
FirstName , ya que los usuarios que van a escribir consultas ad hoc en la base de datos están acostumbrados a
ese nombre. Para realizar esta asignación, puede usar el atributo Column .
El atributo Column especifica que, cuando se cree la base de datos, la columna de la tabla Student que se asigna
a la propiedad FirstMidName se denominará FirstName . En otras palabras, cuando el código hace referencia a
Student.FirstMidName , los datos procederán o se actualizarán en la columna FirstName de la tabla Student . Si
no especifica nombres de columna, se les asigna el mismo nombre que el de la propiedad.
En el archivo Student.cs, agregue una instrucción using para System.ComponentModel.DataAnnotations.Schema y
agregue el atributo de nombre de columna a la propiedad FirstMidName , como se muestra en el código
resaltado siguiente:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
public class Student
{
public int ID { get; set; }
[StringLength(50)]
public string LastName { get; set; }
[StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
[Column("FirstName")]
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime EnrollmentDate { get; set; }

public ICollection<Enrollment> Enrollments { get; set; }


}
}

La adición del atributo Column cambia el modelo de respaldo de SchoolContext , por lo que no coincidirá con la
base de datos.
Guarde los cambios y compile el proyecto. Después, abra la ventana de comandos en la carpeta de proyecto y
escriba los comandos siguientes para crear otra migración:

dotnet ef migrations add ColumnFirstName

dotnet ef database update

En el Explorador de objetos de SQL Server, abra el diseñador de tablas de Student haciendo doble clic en la
tabla Student.

Antes de aplicar las dos primeras migraciones, las columnas de nombre eran de tipo nvarchar(MAX). Ahora son
de tipo nvarchar(50) y el nombre de columna ha cambiado de FirstMidName a FirstName.
NOTE
Si intenta compilar antes de terminar de crear todas las clases de entidad en las secciones siguientes, es posible que se
produzcan errores del compilador.

Cambios finales a la entidad Student

En Models/Student.cs, reemplace el código que agregó anteriormente con el código siguiente. Los cambios
aparecen resaltados.

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
public class Student
{
public int ID { get; set; }
[Required]
[StringLength(50)]
[Display(Name = "Last Name")]
public string LastName { get; set; }
[Required]
[StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
[Column("FirstName")]
[Display(Name = "First Name")]
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
[Display(Name = "Enrollment Date")]
public DateTime EnrollmentDate { get; set; }
[Display(Name = "Full Name")]
public string FullName
{
get
{
return LastName + ", " + FirstMidName;
}
}

public ICollection<Enrollment> Enrollments { get; set; }


}
}

El atributo Required
El atributo Required hace que las propiedades de nombre sean campos obligatorios. El atributo Required no es
necesario para los tipos que no aceptan valores NULL, como los tipos de valor (DateTime, int, double, float, etc.).
Los tipos que no aceptan valores NULL se tratan automáticamente como campos obligatorios.
Puede quitar el atributo Required y reemplazarlo por un parámetro de longitud mínima para el atributo
StringLength :

[Display(Name = "Last Name")]


[StringLength(50, MinimumLength=1)]
public string LastName { get; set; }

El atributo Display
El atributo Display especifica que el título de los cuadros de texto debe ser "First Name" (Nombre), "Last
Name" (Apellidos), "Full Name" (Nombre completo) y "Enrollment Date" (Fecha de inscripción) en lugar del
nombre de propiedad de cada instancia (que no tiene ningún espacio para dividir las palabras).
La propiedad calculada FullName
FullName es una propiedad calculada que devuelve un valor que se crea mediante la concatenación de otras dos
propiedades. Por tanto, solo tiene un descriptor de acceso get y no se generará ninguna columna FullName en la
base de datos.

Crear la entidad Instructor

Cree Models/Instructor.cs y reemplace el código de plantilla con el código siguiente:


using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
public class Instructor
{
public int ID { get; set; }

[Required]
[Display(Name = "Last Name")]
[StringLength(50)]
public string LastName { get; set; }

[Required]
[Column("FirstName")]
[Display(Name = "First Name")]
[StringLength(50)]
public string FirstMidName { get; set; }

[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
[Display(Name = "Hire Date")]
public DateTime HireDate { get; set; }

[Display(Name = "Full Name")]


public string FullName
{
get { return LastName + ", " + FirstMidName; }
}

public ICollection<CourseAssignment> CourseAssignments { get; set; }


public OfficeAssignment OfficeAssignment { get; set; }
}
}

Tenga en cuenta que varias propiedades son las mismas en las entidades Instructor y Student. En el tutorial
Implementación de la herencia más adelante en esta serie, deberá refactorizar este código para eliminar la
redundancia.
Puede colocar varios atributos en una línea, por lo que también puede escribir los atributos HireDate como se
indica a continuación:

[DataType(DataType.Date),Display(Name = "Hire Date"),DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}",


ApplyFormatInEditMode = true)]

Las propiedades de navegación CourseAssignments y OfficeAssignment


CourseAssignments y OfficeAssignment son propiedades de navegación.

Un instructor puede impartir cualquier número de cursos, por lo que CourseAssignments se define como una
colección.

public ICollection<CourseAssignment> CourseAssignments { get; set; }

Si una propiedad de navegación puede contener varias entidades, su tipo debe ser una lista a la que se puedan
agregar, eliminar y actualizar entradas. Puede especificar ICollection<T> o un tipo como List<T> o HashSet<T> .
Si especifica ICollection<T> , EF crea una colección HashSet<T> de forma predeterminada.
El motivo por el que se trata de entidades CourseAssignment se explica a continuación, en la sección sobre
relaciones de varios a varios.
Las reglas de negocio de Contoso University establecen que un instructor solo puede tener una oficina a lo
sumo, por lo que la propiedad OfficeAssignment contiene una única entidad OfficeAssignment (que puede ser
NULL si no se asigna ninguna oficina).

public OfficeAssignment OfficeAssignment { get; set; }

Crear la entidad OfficeAssignment

Cree Models/OfficeAssignment.cs con el código siguiente:

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
public class OfficeAssignment
{
[Key]
public int InstructorID { get; set; }
[StringLength(50)]
[Display(Name = "Office Location")]
public string Location { get; set; }

public Instructor Instructor { get; set; }


}
}

El atributo Key
Hay una relación de uno a cero o uno entre las entidades Instructor y OfficeAssignment. Solo existe una
asignación de oficina en relación con el instructor al que se asigna y, por tanto, su clave principal también es su
clave externa para la entidad Instructor. Pero Entity Framework no reconoce automáticamente InstructorID
como la clave principal de esta entidad porque su nombre no sigue la convención de nomenclatura de ID o
classnameID. Por tanto, se usa el atributo Key para identificarla como la clave:

[Key]
public int InstructorID { get; set; }

También puede usar el atributo Key si la entidad tiene su propia clave principal, pero querrá asignar un nombre
a la propiedad que no sea classnameID o ID.
De forma predeterminada, EF trata la clave como no generada por la base de datos porque la columna es para
una relación de identificación.
La propiedad de navegación Instructor
La entidad Instructor tiene una propiedad de navegación OfficeAssignment que acepta valores NULL (porque es
posible que no se asigne una oficina a un instructor), y la entidad OfficeAssignment tiene una propiedad de
navegación Instructor que no acepta valores NULL (porque una asignación de oficina no puede existir sin un
instructor; InstructorID no acepta valores NULL ). Cuando una entidad Instructor tiene una entidad
OfficeAssignment relacionada, cada entidad tendrá una referencia a la otra en su propiedad de navegación.
Podría incluir un atributo [Required] en la propiedad de navegación de Instructor para especificar que debe
haber un instructor relacionado, pero no es necesario hacerlo porque la clave externa InstructorID (que
también es la clave para esta tabla) no acepta valores NULL.

Modificar la entidad Course

En Models/Course.cs, reemplace el código que agregó anteriormente con el código siguiente. Los cambios
aparecen resaltados.

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
public class Course
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }

[StringLength(50, MinimumLength = 3)]


public string Title { get; set; }

[Range(0, 5)]
public int Credits { get; set; }

public int DepartmentID { get; set; }

public Department Department { get; set; }


public ICollection<Enrollment> Enrollments { get; set; }
public ICollection<CourseAssignment> CourseAssignments { get; set; }
}
}

La entidad Course tiene una propiedad de clave externa DepartmentID que señala a la entidad Department
relacionada y tiene una propiedad de navegación Department .
Entity Framework no requiere que agregue una propiedad de clave externa al modelo de datos cuando tenga
una propiedad de navegación para una entidad relacionada. EF crea automáticamente claves externas en la base
de datos siempre que se necesiten y crea propiedades reemplazadas para ellas. Pero tener la clave externa en el
modelo de datos puede hacer que las actualizaciones sean más sencillas y eficaces. Por ejemplo, al recuperar una
entidad Course para modificarla, la entidad Department es NULL si no la carga, por lo que cuando se actualiza
la entidad Course, deberá capturar primero la entidad Department. Cuando la propiedad de clave externa
DepartmentID se incluye en el modelo de datos, no es necesario capturar la entidad Department antes de
actualizar.
El atributo DatabaseGenerated
El atributo DatabaseGenerated con el parámetro None en la propiedad CourseID especifica que los valores de
clave principal los proporciona el usuario, en lugar de que los genere la base de datos.

[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }

De forma predeterminada, Entity Framework da por supuesto que la base de datos genera los valores de clave
principal. Es lo que le interesa en la mayoría de los escenarios. Pero para las entidades Course, usará un número
de curso especificado por el usuario como una serie 1000 para un departamento, una serie 2000 para otro y así
sucesivamente.
También se puede usar el atributo DatabaseGenerated para generar valores predeterminados, como en el caso de
las columnas de base de datos que se usan para registrar la fecha de creación o actualización de una fila. Para
obtener más información, vea Propiedades generadas.
Propiedades de clave externa y de navegación
Las propiedades de clave externa y las de navegación de la entidad Course reflejan las relaciones siguientes:
Un curso se asigna a un departamento, por lo que hay una clave externa DepartmentID y una propiedad de
navegación Department por las razones mencionadas anteriormente.

public int DepartmentID { get; set; }


public Department Department { get; set; }

Un curso puede tener cualquier número de alumnos inscritos en él, por lo que la propiedad de navegación
Enrollments es una colección:

public ICollection<Enrollment> Enrollments { get; set; }

Un curso puede ser impartido por varios instructores, por lo que la propiedad de navegación CourseAssignments
es una colección (el tipo CourseAssignment se explica más adelante):

public ICollection<CourseAssignment> CourseAssignments { get; set; }

Crear la entidad Department


Cree Models/Department.cs con el código siguiente:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
public class Department
{
public int DepartmentID { get; set; }

[StringLength(50, MinimumLength = 3)]


public string Name { get; set; }

[DataType(DataType.Currency)]
[Column(TypeName = "money")]
public decimal Budget { get; set; }

[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
[Display(Name = "Start Date")]
public DateTime StartDate { get; set; }

public int? InstructorID { get; set; }

public Instructor Administrator { get; set; }


public ICollection<Course> Courses { get; set; }
}
}

El atributo Column
Anteriormente usó el atributo Column para cambiar la asignación de nombres de columna. En el código de la
entidad Department, se usa el atributo Column para cambiar la asignación de tipos de datos de SQL para que la
columna se defina con el tipo de moneda de SQL Server en la base de datos:

[Column(TypeName="money")]
public decimal Budget { get; set; }

Por lo general, la asignación de columnas no es necesaria, dado que Entity Framework elige el tipo de datos de
SQL Server adecuado en función del tipo CLR que se defina para la propiedad. El tipo CLR decimal se asigna a
un tipo decimal de SQL Server. Pero en este caso se sabe que la columna va a contener cantidades de divisa, y
el tipo de datos de divisa es más adecuado para eso.
Propiedades de clave externa y de navegación
Las propiedades de clave externa y de navegación reflejan las relaciones siguientes:
Un departamento puede tener o no un administrador, y un administrador es siempre un instructor. Por tanto, la
propiedad InstructorID se incluye como la clave externa de la entidad Instructor y se agrega un signo de
interrogación después de la designación del tipo int para marcar la propiedad como que acepta valores NULL.
La propiedad de navegación se denomina Administrator pero contiene una entidad Instructor:

public int? InstructorID { get; set; }


public Instructor Administrator { get; set; }

Un departamento puede tener varios cursos, por lo que hay una propiedad de navegación Courses:
public ICollection<Course> Courses { get; set; }

NOTE
Por convención, Entity Framework permite la eliminación en cascada para las claves externas que no aceptan valores NULL
y para las relaciones de varios a varios. Esto puede dar lugar a reglas de eliminación en cascada circulares, lo que producirá
una excepción al intentar agregar una migración. Por ejemplo, si no definió la propiedad Department.InstructorID como
que acepta valores NULL, EF podría configurar una regla de eliminación en cascada para eliminar el instructor cuando se
elimine el departamento, que no es lo que quiere que ocurra. Si las reglas de negocio requerían que la propiedad
InstructorID no acepte valores NULL, tendría que usar la siguiente instrucción de la API fluida para deshabilitar la
eliminación en cascada en la relación:

modelBuilder.Entity<Department>()
.HasOne(d => d.Administrator)
.WithMany()
.OnDelete(DeleteBehavior.Restrict)

Modificar la entidad Enrollment

En Models/Enrollment.cs, reemplace el código que agregó anteriormente con el código siguiente:

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
public enum Grade
{
A, B, C, D, F
}

public class Enrollment


{
public int EnrollmentID { get; set; }
public int CourseID { get; set; }
public int StudentID { get; set; }
[DisplayFormat(NullDisplayText = "No grade")]
public Grade? Grade { get; set; }

public Course Course { get; set; }


public Student Student { get; set; }
}
}

Propiedades de clave externa y de navegación


Las propiedades de clave externa y de navegación reflejan las relaciones siguientes:
Un registro de inscripción es para un solo curso, por lo que hay una propiedad de clave externa CourseID y una
propiedad de navegación Course :

public int CourseID { get; set; }


public Course Course { get; set; }

Un registro de inscripción es para un solo estudiante, por lo que hay una propiedad de clave externa StudentID
y una propiedad de navegación Student :

public int StudentID { get; set; }


public Student Student { get; set; }

Relaciones Varios a Varios


Hay una relación de varios a varios entre las entidades Student y Course, y la entidad Enrollment funciona como
una tabla de combinación de varios a varios con carga en la base de datos. "Con carga" significa que la tabla
Enrollment contiene datos adicionales además de las claves externas para las tablas combinadas (en este caso,
una clave principal y una propiedad Grade).
En la ilustración siguiente se muestra el aspecto de estas relaciones en un diagrama de entidades. (Este
diagrama se ha generado mediante Entity Framework Power Tools para EF 6.x; la creación del diagrama no
forma parte del tutorial, simplemente se usa aquí como una ilustración).

Cada línea de relación tiene un 1 en un extremo y un asterisco (*) en el otro, para indicar una relación uno a
varios.
Si la tabla Enrollment no incluyera información de calificaciones, solo tendría que contener las dos claves
externas CourseID y StudentID. En ese caso, sería una tabla de combinación de varios a varios sin carga (o una
tabla de combinación pura) en la base de datos. Las entidades Instructor y Course tienen ese tipo de relación de
varios a varios, y el paso siguiente consiste en crear una clase de entidad para que funcione como una tabla de
combinación sin carga.
(EF 6.x es compatible con las tablas de combinación implícitas para relaciones de varios a varios, pero EF Core
no. Para obtener más información, vea la explicación en el repositorio de GitHub EF Core).

La entidad CourseAssignment

Cree Models/CourseAssignment.cs con el código siguiente:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
public class CourseAssignment
{
public int InstructorID { get; set; }
public int CourseID { get; set; }
public Instructor Instructor { get; set; }
public Course Course { get; set; }
}
}

Nombres de entidades de combinación


Se requiere una tabla de combinación en la base de datos para la relación de varios a varios entre Instructor y
Courses, y se tiene que representar mediante un conjunto de entidades. Es habitual asignar el nombre
EntityName1EntityName2 a una entidad de combinación, que en este caso sería CourseInstructor . Pero se
recomienda elegir un nombre que describa la relación. Los modelos de datos empiezan de manera sencilla y
crecen, y las combinaciones sin carga suelen obtener las cargas más tarde. Si empieza con un nombre de entidad
descriptivo, no tendrá que cambiarlo más adelante. Idealmente, la entidad de combinación tendrá su propio
nombre natural (posiblemente una sola palabra) en el dominio de empresa. Por ejemplo, Books y Customers
podrían vincularse a través de Ratings. Para esta relación, CourseAssignment es una opción más adecuada que
CourseInstructor .

Clave compuesta
Puesto que las claves externas no aceptan valores NULL y juntas identifican de forma única a cada fila de la
tabla, una clave principal independiente no es necesaria. Las propiedades InstructorID y CourseID deben
funcionar como una clave principal compuesta. La única manera de identificar claves principales compuestas
para EF es mediante la API fluida (no se puede realizar mediante el uso de atributos). En la sección siguiente
verá cómo configurar la clave principal compuesta.
La clave compuesta garantiza que, aunque es posible tener varias filas para un curso y varias filas para un
instructor, no se pueden tener varias filas para el mismo instructor y curso. La entidad de combinación
Enrollment define su propia clave principal, por lo que este tipo de duplicados son posibles. Para evitar estos
duplicados, podría agregar un índice único en los campos de clave externa o configurar Enrollment con una
clave principal compuesta similar a CourseAssignment . Para obtener más información, vea Índices.
Actualizar el contexto de base de datos
Agregue el código resaltado siguiente al archivo Data/SchoolContext.cs:

using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity.Data
{
public class SchoolContext : DbContext
{
public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
{
}

public DbSet<Course> Courses { get; set; }


public DbSet<Enrollment> Enrollments { get; set; }
public DbSet<Student> Students { get; set; }
public DbSet<Department> Departments { get; set; }
public DbSet<Instructor> Instructors { get; set; }
public DbSet<OfficeAssignment> OfficeAssignments { get; set; }
public DbSet<CourseAssignment> CourseAssignments { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)


{
modelBuilder.Entity<Course>().ToTable("Course");
modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
modelBuilder.Entity<Student>().ToTable("Student");
modelBuilder.Entity<Department>().ToTable("Department");
modelBuilder.Entity<Instructor>().ToTable("Instructor");
modelBuilder.Entity<OfficeAssignment>().ToTable("OfficeAssignment");
modelBuilder.Entity<CourseAssignment>().ToTable("CourseAssignment");

modelBuilder.Entity<CourseAssignment>()
.HasKey(c => new { c.CourseID, c.InstructorID });
}
}
}

Este código agrega las nuevas entidades y configura la clave principal compuesta de la entidad
CourseAssignment.

Alternativa de la API fluida a los atributos


En el código del método OnModelCreating de la clase DbContext se usa la API fluida para configurar el
comportamiento de EF. La API se denomina "fluida" porque a menudo se usa para encadenar una serie de
llamadas de método en una única instrucción, como en este ejemplo de la documentación de EF Core:

protected override void OnModelCreating(ModelBuilder modelBuilder)


{
modelBuilder.Entity<Blog>()
.Property(b => b.Url)
.IsRequired();
}

En este tutorial, solo se usa la API fluida para la asignación de base de datos que no se puede realizar con
atributos. Pero también se puede usar la API fluida para especificar casi todas las reglas de formato, validación y
asignación que se pueden realizar mediante el uso de atributos. Algunos atributos como MinimumLength no se
pueden aplicar con la API fluida. Como se mencionó anteriormente, MinimumLength no cambia el esquema, solo
aplica una regla de validación del lado cliente y del lado servidor.
Algunos desarrolladores prefieren usar la API fluida exclusivamente para mantener "limpias" las clases de
entidad. Si quiere, puede mezclar atributos y la API fluida, y hay algunas personalizaciones que solo se pueden
realizar mediante la API fluida, pero en general el procedimiento recomendado es elegir uno de estos dos
enfoques y usarlo de forma constante siempre que sea posible. Si usa ambos enfoques, tenga en cuenta que
siempre que hay un conflicto, la API fluida invalida los atributos.
Para obtener más información sobre la diferencia entre los atributos y la API fluida, vea Métodos de
configuración.

Diagrama de entidades en el que se muestran las relaciones


En la siguiente ilustración se muestra el diagrama creado por Entity Framework Power Tools para el modelo
School completado.

Además de las líneas de relación uno a varios (1 a *), aquí se puede ver la línea de relación de uno a cero o uno
(1 a 0..1) entre las entidades Instructor y OfficeAssignment, y la línea de relación de cero o uno a varios (0..1 a *)
entre las entidades Instructor y Department.

Inicialización de la base de datos con datos de prueba


Reemplace el código del archivo Data/DbInitializer.cs con el código siguiente para proporcionar datos de
inicialización para las nuevas entidades que ha creado.
using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using ContosoUniversity.Models;

namespace ContosoUniversity.Data
{
public static class DbInitializer
{
public static void Initialize(SchoolContext context)
{
//context.Database.EnsureCreated();

// Look for any students.


if (context.Students.Any())
{
return; // DB has been seeded
}

var students = new Student[]


{
new Student { FirstMidName = "Carson", LastName = "Alexander",
EnrollmentDate = DateTime.Parse("2010-09-01") },
new Student { FirstMidName = "Meredith", LastName = "Alonso",
EnrollmentDate = DateTime.Parse("2012-09-01") },
new Student { FirstMidName = "Arturo", LastName = "Anand",
EnrollmentDate = DateTime.Parse("2013-09-01") },
new Student { FirstMidName = "Gytis", LastName = "Barzdukas",
EnrollmentDate = DateTime.Parse("2012-09-01") },
new Student { FirstMidName = "Yan", LastName = "Li",
EnrollmentDate = DateTime.Parse("2012-09-01") },
new Student { FirstMidName = "Peggy", LastName = "Justice",
EnrollmentDate = DateTime.Parse("2011-09-01") },
new Student { FirstMidName = "Laura", LastName = "Norman",
EnrollmentDate = DateTime.Parse("2013-09-01") },
new Student { FirstMidName = "Nino", LastName = "Olivetto",
EnrollmentDate = DateTime.Parse("2005-09-01") }
};

foreach (Student s in students)


{
context.Students.Add(s);
}
context.SaveChanges();

var instructors = new Instructor[]


{
new Instructor { FirstMidName = "Kim", LastName = "Abercrombie",
HireDate = DateTime.Parse("1995-03-11") },
new Instructor { FirstMidName = "Fadi", LastName = "Fakhouri",
HireDate = DateTime.Parse("2002-07-06") },
new Instructor { FirstMidName = "Roger", LastName = "Harui",
HireDate = DateTime.Parse("1998-07-01") },
new Instructor { FirstMidName = "Candace", LastName = "Kapoor",
HireDate = DateTime.Parse("2001-01-15") },
new Instructor { FirstMidName = "Roger", LastName = "Zheng",
HireDate = DateTime.Parse("2004-02-12") }
};

foreach (Instructor i in instructors)


{
context.Instructors.Add(i);
}
context.SaveChanges();

var departments = new Department[]


{
new Department { Name = "English", Budget = 350000,
StartDate = DateTime.Parse("2007-09-01"),
InstructorID = instructors.Single( i => i.LastName == "Abercrombie").ID },
new Department { Name = "Mathematics", Budget = 100000,
StartDate = DateTime.Parse("2007-09-01"),
InstructorID = instructors.Single( i => i.LastName == "Fakhouri").ID },
new Department { Name = "Engineering", Budget = 350000,
StartDate = DateTime.Parse("2007-09-01"),
InstructorID = instructors.Single( i => i.LastName == "Harui").ID },
new Department { Name = "Economics", Budget = 100000,
StartDate = DateTime.Parse("2007-09-01"),
InstructorID = instructors.Single( i => i.LastName == "Kapoor").ID }
};

foreach (Department d in departments)


{
context.Departments.Add(d);
}
context.SaveChanges();

var courses = new Course[]


{
new Course {CourseID = 1050, Title = "Chemistry", Credits = 3,
DepartmentID = departments.Single( s => s.Name == "Engineering").DepartmentID
},
new Course {CourseID = 4022, Title = "Microeconomics", Credits = 3,
DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID
},
new Course {CourseID = 4041, Title = "Macroeconomics", Credits = 3,
DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID
},
new Course {CourseID = 1045, Title = "Calculus", Credits = 4,
DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID
},
new Course {CourseID = 3141, Title = "Trigonometry", Credits = 4,
DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID
},
new Course {CourseID = 2021, Title = "Composition", Credits = 3,
DepartmentID = departments.Single( s => s.Name == "English").DepartmentID
},
new Course {CourseID = 2042, Title = "Literature", Credits = 4,
DepartmentID = departments.Single( s => s.Name == "English").DepartmentID
},
};

foreach (Course c in courses)


{
context.Courses.Add(c);
}
context.SaveChanges();

var officeAssignments = new OfficeAssignment[]


{
new OfficeAssignment {
InstructorID = instructors.Single( i => i.LastName == "Fakhouri").ID,
Location = "Smith 17" },
new OfficeAssignment {
InstructorID = instructors.Single( i => i.LastName == "Harui").ID,
Location = "Gowan 27" },
new OfficeAssignment {
InstructorID = instructors.Single( i => i.LastName == "Kapoor").ID,
Location = "Thompson 304" },
};

foreach (OfficeAssignment o in officeAssignments)


{
context.OfficeAssignments.Add(o);
}
}
context.SaveChanges();

var courseInstructors = new CourseAssignment[]


{
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
InstructorID = instructors.Single(i => i.LastName == "Kapoor").ID
},
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
InstructorID = instructors.Single(i => i.LastName == "Harui").ID
},
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Microeconomics" ).CourseID,
InstructorID = instructors.Single(i => i.LastName == "Zheng").ID
},
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Macroeconomics" ).CourseID,
InstructorID = instructors.Single(i => i.LastName == "Zheng").ID
},
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Calculus" ).CourseID,
InstructorID = instructors.Single(i => i.LastName == "Fakhouri").ID
},
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Trigonometry" ).CourseID,
InstructorID = instructors.Single(i => i.LastName == "Harui").ID
},
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Composition" ).CourseID,
InstructorID = instructors.Single(i => i.LastName == "Abercrombie").ID
},
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Literature" ).CourseID,
InstructorID = instructors.Single(i => i.LastName == "Abercrombie").ID
},
};

foreach (CourseAssignment ci in courseInstructors)


{
context.CourseAssignments.Add(ci);
}
context.SaveChanges();

var enrollments = new Enrollment[]


{
new Enrollment {
StudentID = students.Single(s => s.LastName == "Alexander").ID,
CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
Grade = Grade.A
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Alexander").ID,
CourseID = courses.Single(c => c.Title == "Microeconomics" ).CourseID,
Grade = Grade.C
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Alexander").ID,
CourseID = courses.Single(c => c.Title == "Macroeconomics" ).CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Alonso").ID,
CourseID = courses.Single(c => c.Title == "Calculus" ).CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Alonso").ID,
CourseID = courses.Single(c => c.Title == "Trigonometry" ).CourseID,
CourseID = courses.Single(c => c.Title == "Trigonometry" ).CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Alonso").ID,
CourseID = courses.Single(c => c.Title == "Composition" ).CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Anand").ID,
CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Anand").ID,
CourseID = courses.Single(c => c.Title == "Microeconomics").CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Barzdukas").ID,
CourseID = courses.Single(c => c.Title == "Chemistry").CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Li").ID,
CourseID = courses.Single(c => c.Title == "Composition").CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Justice").ID,
CourseID = courses.Single(c => c.Title == "Literature").CourseID,
Grade = Grade.B
}
};

foreach (Enrollment e in enrollments)


{
var enrollmentInDataBase = context.Enrollments.Where(
s =>
s.Student.ID == e.StudentID &&
s.Course.CourseID == e.CourseID).SingleOrDefault();
if (enrollmentInDataBase == null)
{
context.Enrollments.Add(e);
}
}
context.SaveChanges();
}
}
}

Como vimos en el primer tutorial, la mayor parte de este código simplemente crea objetos de entidad y carga
los datos de ejemplo en propiedades según sea necesario para las pruebas. Tenga en cuenta cómo se
administran las relaciones de varios a varios: el código crea relaciones mediante la creación de entidades en los
conjuntos de entidades de combinación Enrollments y CourseAssignment .

Agregar una migración


Guarde los cambios y compile el proyecto. Después, abra la ventana de comandos en la carpeta del proyecto y
escriba el comando migrations add (no ejecute el comando update-database todavía):

dotnet ef migrations add ComplexDataModel

Recibirá una advertencia sobre la posible pérdida de datos.


An operation was scaffolded that may result in the loss of data. Please review the migration for accuracy.
Done. To undo this action, use 'ef migrations remove'

Si ahora intentara ejecutar el comando database update (no lo haga todavía), obtendría el error siguiente:

Instrucción ALTER TABLE en conflicto con la restricción FOREIGN KEY


"FK_dbo.Course_dbo.Department_DepartmentID". El conflicto ha aparecido en la base de datos
"ContosoUniversity", tabla "dbo.Department", columna "DepartmentID".

En ocasiones, al ejecutar migraciones con datos existentes, debe insertar código auxiliar de los datos en la base
de datos para satisfacer las restricciones de clave externa. El código generado en el método Up agrega a la tabla
Course una clave externa DepartmentID que no acepta valores NULL. Si ya hay filas en la tabla Course cuando
se ejecuta el código, se produce un error en la operación AddColumn porque SQL Server no sabe qué valor
incluir en la columna que no puede ser NULL. En este tutorial se va a ejecutar la migración en una base de datos
nueva, pero en una aplicación de producción la migración tendría que controlar los datos existentes, por lo que
en las instrucciones siguientes se muestra un ejemplo de cómo hacerlo.
Para realizar este trabajo de migración con datos existentes, tendrá que cambiar el código para asignar un valor
predeterminado a la nueva columna y crear un departamento de código auxiliar denominado "Temp" para que
actúe como el predeterminado. Como resultado, las filas Course existentes estarán relacionadas con el
departamento "Temp" después de ejecutar el método Up .
Abra el archivo {marca_de_tiempo }_ComplexDataModel.cs.
Convierta en comentario la línea de código que agrega la columna DepartmentID a la tabla Course.

migrationBuilder.AlterColumn<string>(
name: "Title",
table: "Course",
maxLength: 50,
nullable: true,
oldClrType: typeof(string),
oldNullable: true);

//migrationBuilder.AddColumn<int>(
// name: "DepartmentID",
// table: "Course",
// nullable: false,
// defaultValue: 0);

Agregue el código resaltado siguiente después del código que crea la tabla Department:
migrationBuilder.CreateTable(
name: "Department",
columns: table => new
{
DepartmentID = table.Column<int>(nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy",
SqlServerValueGenerationStrategy.IdentityColumn),
Budget = table.Column<decimal>(type: "money", nullable: false),
InstructorID = table.Column<int>(nullable: true),
Name = table.Column<string>(maxLength: 50, nullable: true),
StartDate = table.Column<DateTime>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Department", x => x.DepartmentID);
table.ForeignKey(
name: "FK_Department_Instructor_InstructorID",
column: x => x.InstructorID,
principalTable: "Instructor",
principalColumn: "ID",
onDelete: ReferentialAction.Restrict);
});

migrationBuilder.Sql("INSERT INTO dbo.Department (Name, Budget, StartDate) VALUES ('Temp', 0.00,


GETDATE())");
// Default value for FK points to department created above, with
// defaultValue changed to 1 in following AddColumn statement.

migrationBuilder.AddColumn<int>(
name: "DepartmentID",
table: "Course",
nullable: false,
defaultValue: 1);

En una aplicación de producción, debería escribir código o scripts para agregar filas Department y filas Course
relacionadas a las nuevas filas Department. Después, ya no necesitaría el departamento "Temp" o el valor
predeterminado en la columna Course.DepartmentID.
Guarde los cambios y compile el proyecto.

Cambiar la cadena de conexión y actualizar la base de datos


Ahora tiene código nuevo en la clase DbInitializer que agrega datos de inicialización para las nuevas
entidades a una base de datos vacía. Para asegurarse de que EF crea una base de datos vacía, cambie el nombre
de la base de datos en la cadena de conexión en appsettings.json por ContosoUniversity3 u otro nombre que no
haya usado en el equipo que esté usando.

{
"ConnectionStrings": {
"DefaultConnection": "Server=
(localdb)\\mssqllocaldb;Database=ContosoUniversity3;Trusted_Connection=True;MultipleActiveResultSets=true"
},

Guarde el cambio en appsettings.json.


NOTE
Como alternativa a cambiar el nombre de la base de datos, puede eliminar la base de datos. Use el Explorador de
objetos de SQL Server (SSOX) o el comando de la CLI database drop :

dotnet ef database drop

Después de cambiar el nombre de la base de datos o de eliminarla, ejecute el comando database update en la
ventana de comandos para ejecutar las migraciones.

dotnet ef database update

Ejecute la aplicación para que el método DbInitializer.Initialize ejecute y rellene la base de datos nueva.
Abra la base de datos en SSOX como hizo anteriormente y expanda el nodo Tablas para ver que se han creado
todas las tablas. (Si SSOX sigue abierto de la vez anterior, haga clic en el botón Actualizar).

Ejecute la aplicación para desencadenar el código de inicialización de la base de datos.


Haga clic con el botón derecho en la tabla CourseAssignment y seleccione Ver datos para comprobar que
contiene datos.
Resumen
Ahora tiene un modelo de datos más complejo y la base de datos correspondiente. En el siguiente tutorial,
obtendrá más información sobre cómo obtener acceso a datos relacionados.

A N T E R IO R S IG U IE N T E
ASP.NET Core MVC con EF Core: Lectura de datos
relacionados (6 de 10)
25/06/2018 • 26 minutes to read • Edit Online

Por Tom Dykstra y Rick Anderson


En la aplicación web de ejemplo Contoso University se muestra cómo crear aplicaciones web de ASP.NET Core
MVC con Entity Framework Core y Visual Studio. Para obtener información sobre la serie de tutoriales, consulte
el primer tutorial de la serie.
En el tutorial anterior, completó el modelo de datos School. En este tutorial podrá leer y mostrar datos
relacionados, es decir, los datos que Entity Framework carga en propiedades de navegación.
En las ilustraciones siguientes se muestran las páginas con las que va a trabajar.
Carga diligente, explícita y diferida de datos relacionados
Existen varias formas para que el software de asignación relacional de objetos (ORM ) como Entity Framework
pueda cargar datos relacionados en las propiedades de navegación de una entidad:
Carga diligente. Cuando se lee la entidad, junto a ella se recuperan datos relacionados. Esto normalmente
da como resultado una única consulta de combinación en la que se recuperan todos los datos que se
necesitan. En Entity Framework Core, la carga diligente se especifica mediante los métodos Include y
ThenInclude .

Puede recuperar algunos de los datos en distintas consultas y EF "corregirá" las propiedades de
navegación. Es decir, EF agrega automáticamente las entidades recuperadas por separado que pertenecen
a propiedades de navegación de entidades recuperadas previamente. Para la consulta que recupera los
datos relacionados, puede usar el método Load en lugar de un método que devuelva una lista o un
objeto, como ToList o Single .

Carga explícita. Cuando la entidad se lee por primera vez, no se recuperan datos relacionados. Se escribe
código que recupera los datos relacionados si son necesarios. Como en el caso de la carga diligente con
consultas independientes, la carga explícita da como resultado varias consultas que se envían a la base de
datos. La diferencia es que con la carga explícita el código especifica las propiedades de navegación que
se van a cargar. En Entity Framework Core 1.1 se puede usar el método Load para realizar la carga
explícita. Por ejemplo:

Carga diferida. Cuando la entidad se lee por primera vez, no se recuperan datos relacionados. Pero la
primera vez que intente obtener acceso a una propiedad de navegación, se recuperan automáticamente
los datos necesarios para esa propiedad de navegación. Cada vez que intente obtener datos de una
propiedad de navegación por primera vez, se envía una consulta a la base de datos. Entity Framework
Core 1.0 no admite la carga diferida.
Consideraciones sobre el rendimiento
Si sabe que necesita datos relacionados para cada entidad que se recupere, la carga diligente suele ofrecer el
mejor rendimiento, dado que una sola consulta que se envía a la base de datos normalmente es más eficaz que
consultas independientes para cada entidad recuperada. Por ejemplo, suponga que cada departamento tiene
diez cursos relacionados. Con la carga diligente de todos los datos relacionados se crearía una única consulta
sencilla (de combinación) y un único recorrido de ida y vuelta a la base de datos. Una consulta independiente
para los cursos de cada departamento crearía 11 recorridos de ida y vuelta a la base de datos. Los recorridos de
ida y vuelta adicionales a la base de datos afectan especialmente de forma negativa al rendimiento cuando la
latencia es alta.
Por otro lado, en algunos escenarios, las consultas independientes son más eficaces. Es posible que la carga
diligente de todos los datos relacionados en una consulta genere una combinación muy compleja que SQL
Server no pueda procesar eficazmente. O bien, si necesita tener acceso a las propiedades de navegación de una
entidad solo para un subconjunto de un conjunto de las entidades que está procesando, es posible que las
consultas independientes den mejores resultados porque la carga diligente de todo el contenido por adelantado
recuperaría más datos de los que necesita. Si el rendimiento es crítico, es mejor probarlo de ambas formas para
elegir la mejor opción.

Crear una página de cursos en la que se muestre el nombre de


departamento
La entidad Course incluye una propiedad de navegación que contiene la entidad Department del departamento
al que se asigna el curso. Para mostrar el nombre del departamento asignado en una lista de cursos, tendrá que
obtener la propiedad Name de la entidad Department que se encuentra en la propiedad de navegación
Course.Department .

Cree un controlador denominado CoursesController para el tipo de entidad Course, con las mismas opciones
para el proveedor de scaffolding Controlador de MVC con vistas que usan Entity Framework que usó
anteriormente para el controlador de Students, como se muestra en la ilustración siguiente:

Abra CoursesController.cs y examine el método Index . El scaffolding automático ha especificado la carga


diligente para la propiedad de navegación Department mediante el método Include .
Reemplace el método Index con el siguiente código, en el que se usa un nombre más adecuado para la
IQueryable que devuelve las entidades Course ( courses en lugar de schoolContext ):

public async Task<IActionResult> Index()


{
var courses = _context.Courses
.Include(c => c.Department)
.AsNoTracking();
return View(await courses.ToListAsync());
}

Abra Views/Courses/Index.cshtml y reemplace el código de plantilla con el código siguiente. Se resaltan los
cambios:
@model IEnumerable<ContosoUniversity.Models.Course>

@{
ViewData["Title"] = "Courses";
}

<h2>Courses</h2>

<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.CourseID)
</th>
<th>
@Html.DisplayNameFor(model => model.Title)
</th>
<th>
@Html.DisplayNameFor(model => model.Credits)
</th>
<th>
@Html.DisplayNameFor(model => model.Department)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.CourseID)
</td>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.Credits)
</td>
<td>
@Html.DisplayFor(modelItem => item.Department.Name)
</td>
<td>
<a asp-action="Edit" asp-route-id="@item.CourseID">Edit</a> |
<a asp-action="Details" asp-route-id="@item.CourseID">Details</a> |
<a asp-action="Delete" asp-route-id="@item.CourseID">Delete</a>
</td>
</tr>
}
</tbody>
</table>

Ha realizado los cambios siguientes en el código con scaffolding:


Ha cambiado el título de Index a Courses.
Ha agregado una columna Number en la que se muestra el valor de propiedad CourseID . De forma
predeterminada, las claves principales no tienen scaffolding porque normalmente no tienen sentido para
los usuarios finales. Pero en este caso, la clave principal es significativa y quiere mostrarla.
Ha cambiado la columna Department para mostrar el nombre del departamento. El código muestra la
propiedad Name de la entidad Department que se carga en la propiedad de navegación Department :
@Html.DisplayFor(modelItem => item.Department.Name)

Ejecute la aplicación y haga clic en la pestaña Courses para ver la lista con los nombres de departamento.

Crear una página de instructores en la que se muestran los cursos y


las inscripciones
En esta sección, creará un controlador y una vista de la entidad Instructor con el fin de mostrar la página
Instructors:
En esta página se leen y muestran los datos relacionados de las maneras siguientes:
En la lista de instructores se muestran datos relacionados de la entidad OfficeAssignment. Las entidades
Instructor y OfficeAssignment se encuentran en una relación de uno a cero o uno. Usará la carga diligente
para las entidades OfficeAssignment. Como se explicó anteriormente, la carga diligente normalmente es
más eficaz cuando se necesitan los datos relacionados para todas las filas recuperadas de la tabla
principal. En este caso, quiere mostrar las asignaciones de oficina para todos los instructores que se
muestran.
Cuando el usuario selecciona un instructor, se muestran las entidades Course relacionadas. Las entidades
Instructor y Course se encuentran en una relación de varios a varios. Usará la carga diligente para las
entidades Course y sus entidades Department relacionadas. En este caso, es posible que las consultas
independientes sean más eficaces porque necesita cursos solo para el instructor seleccionado. Pero en
este ejemplo se muestra cómo usar la carga diligente para propiedades de navegación dentro de
entidades que, a su vez, se encuentran en propiedades de navegación.
Cuando el usuario selecciona un curso, se muestran los datos relacionados del conjunto de entidades
Enrollments. Las entidades Course y Enrollment están en una relación uno a varios. Usará consultas
independientes para las entidades Enrollment y sus entidades Student relacionadas.
Crear un modelo de vista para la vista de índice de instructores
En la página Instructors se muestran datos de tres tablas diferentes. Por tanto, creará un modelo de vista que
incluye tres propiedades, cada una con los datos de una de las tablas.
En la carpeta SchoolViewModels, cree InstructorIndexData.cs y reemplace el código existente con el código
siguiente:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Models.SchoolViewModels
{
public class InstructorIndexData
{
public IEnumerable<Instructor> Instructors { get; set; }
public IEnumerable<Course> Courses { get; set; }
public IEnumerable<Enrollment> Enrollments { get; set; }
}
}

Crear el controlador y las vistas de Instructor


Cree un controlador Instructors con acciones de lectura y escritura de EF como se muestra en la ilustración
siguiente:

Abra InstructorsController.cs y agregue una instrucción using para el espacio de nombres ViewModels:

using ContosoUniversity.Models.SchoolViewModels;

Reemplace el método Index con el código siguiente para realizar la carga diligente de los datos relacionados y
colocarlos en el modelo de vista.
public async Task<IActionResult> Index(int? id, int? courseID)
{
var viewModel = new InstructorIndexData();
viewModel.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Enrollments)
.ThenInclude(i => i.Student)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();

if (id != null)
{
ViewData["InstructorID"] = id.Value;
Instructor instructor = viewModel.Instructors.Where(
i => i.ID == id.Value).Single();
viewModel.Courses = instructor.CourseAssignments.Select(s => s.Course);
}

if (courseID != null)
{
ViewData["CourseID"] = courseID.Value;
viewModel.Enrollments = viewModel.Courses.Where(
x => x.CourseID == courseID).Single().Enrollments;
}

return View(viewModel);
}

El método acepta datos de ruta opcionales ( id ) y un parámetro de cadena de consulta ( courseID ) que
proporcionan los valores ID del instructor y el curso seleccionados. Los parámetros se proporcionan mediante
los hipervínculos Select de la página.
El código comienza creando una instancia del modelo de vista y coloca en ella la lista de instructores. El código
especifica la carga diligente para Instructor.OfficeAssignment y las propiedades de navegación de
Instructor.CourseAssignments . Dentro de la propiedad CourseAssignments se carga la propiedad Course y
dentro de esta se cargan las propiedades Enrollments y Department , y dentro de cada entidad Enrollment se
carga la propiedad Student .

viewModel.Instructors = await _context.Instructors


.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Enrollments)
.ThenInclude(i => i.Student)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();

Como la vista siempre requiere la entidad OfficeAssignment, resulta más eficaz capturarla en la misma consulta.
Las entidades Course son necesarias cuando se selecciona un instructor en la página web, por lo que una sola
consulta es más adecuada que varias solo si la página se muestra con más frecuencia con un curso seleccionado
que sin él.
El código repite CourseAssignments y Course porque se necesitan dos propiedades de Course . En la primera
cadena de llamadas ThenInclude se obtiene CourseAssignment.Course , Course.Enrollments y
Enrollment.Student .

viewModel.Instructors = await _context.Instructors


.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Enrollments)
.ThenInclude(i => i.Student)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();

En ese punto del código, otro elemento ThenInclude sería para las propiedades de navegación de Student , lo
que no es necesario. Pero la llamada a Include se inicia con las propiedades de Instructor , por lo que tendrá
que volver a pasar por la cadena, especificando esta vez Course.Department en lugar de Course.Enrollments .

viewModel.Instructors = await _context.Instructors


.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Enrollments)
.ThenInclude(i => i.Student)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();

El código siguiente se ejecuta cuando se ha seleccionado un instructor. El instructor seleccionado se recupera de


la lista de instructores del modelo de vista. Después, se carga la propiedad Courses del modelo de vista con las
entidades Course de la propiedad de navegación CourseAssignments de ese instructor.

if (id != null)
{
ViewData["InstructorID"] = id.Value;
Instructor instructor = viewModel.Instructors.Where(
i => i.ID == id.Value).Single();
viewModel.Courses = instructor.CourseAssignments.Select(s => s.Course);
}

El método Where devuelve una colección, pero en este caso los criterios que se pasan a ese método dan como
resultado que solo se devuelva una entidad Instructor. El método Single convierte la colección en una única
entidad Instructor, que proporciona acceso a la propiedad CourseAssignments de esa entidad. La propiedad
CourseAssignments contiene entidades CourseAssignment , de las que solo quiere las entidades Course
relacionadas.
El método Single se usa en una colección cuando se sabe que la colección tendrá un único elemento. El método
Single inicia una excepción si la colección que se pasa está vacía o si hay más de un elemento. Una alternativa es
SingleOrDefault , que devuelve una valor predeterminado ( NULL, en este caso) si la colección está vacía. Pero en
este caso, eso seguiría iniciando una excepción (al tratar de buscar una propiedad Courses en una referencia
nula), y el mensaje de excepción indicaría con menos claridad la causa del problema. Cuando se llama al método
Single , también se puede pasar la condición Where en lugar de llamar al método Where por separado:

.Single(i => i.ID == id.Value)

En lugar de:

.Where(I => i.ID == id.Value).Single()

A continuación, si se ha seleccionado un curso, se recupera de la lista de cursos en el modelo de vista. Después,


se carga la propiedad Enrollments del modelo de vista con las entidades Enrollment de la propiedad de
navegación Enrollments de ese curso.

if (courseID != null)
{
ViewData["CourseID"] = courseID.Value;
viewModel.Enrollments = viewModel.Courses.Where(
x => x.CourseID == courseID).Single().Enrollments;
}

Modificar la vista de índice de instructores


En Views/Instructors/Index.cshtml, reemplace el código de plantilla con el código siguiente. Los cambios
aparecen resaltados.
@model ContosoUniversity.Models.SchoolViewModels.InstructorIndexData

@{
ViewData["Title"] = "Instructors";
}

<h2>Instructors</h2>

<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>Last Name</th>
<th>First Name</th>
<th>Hire Date</th>
<th>Office</th>
<th>Courses</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Instructors)
{
string selectedRow = "";
if (item.ID == (int?)ViewData["InstructorID"])
{
selectedRow = "success";
}
<tr class="@selectedRow">
<td>
@Html.DisplayFor(modelItem => item.LastName)
</td>
<td>
@Html.DisplayFor(modelItem => item.FirstMidName)
</td>
<td>
@Html.DisplayFor(modelItem => item.HireDate)
</td>
<td>
@if (item.OfficeAssignment != null)
{
@item.OfficeAssignment.Location
}
</td>
<td>
@{
foreach (var course in item.CourseAssignments)
{
@course.Course.CourseID @: @course.Course.Title <br />
}
}
</td>
<td>
<a asp-action="Index" asp-route-id="@item.ID">Select</a> |
<a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-action="Details" asp-route-id="@item.ID">Details</a> |
<a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>

Ha realizado los cambios siguientes en el código existente:


Ha cambiado la clase de modelo por InstructorIndexData .
Ha cambiado el título de la página de Index a Instructors.
Se ha agregado una columna Office en la que se muestra item.OfficeAssignment.Location solo si
item.OfficeAssignment no es NULL. ( Dado que se trata de una relación de uno a cero o uno, es posible
que no haya una entidad OfficeAssignment relacionada).

@if (item.OfficeAssignment != null)


{
@item.OfficeAssignment.Location
}

Se ha agregado una columna Courses en la que se muestran los cursos que imparte cada instructor. Vea
Transición de línea explícita con @: para obtener más información sobre esta sintaxis de Razor.
Ha agregado código que agrega dinámicamente class="success" al elemento tr del instructor
seleccionado. Esto establece el color de fondo de la fila seleccionada mediante una clase de arranque.

string selectedRow = "";


if (item.ID == (int?)ViewData["InstructorID"])
{
selectedRow = "success";
}
<tr class="@selectedRow">

Se ha agregado un hipervínculo nuevo con la etiqueta Select inmediatamente antes de los otros vínculos
de cada fila, lo que hace que el identificador del instructor seleccionado se envíe al método Index .

<a asp-action="Index" asp-route-id="@item.ID">Select</a> |

Ejecute la aplicación y haga clic en la pestaña Instructors. En la página se muestra la propiedad Location de las
entidades OfficeAssignment relacionadas y una celda de tabla vacía cuando no hay ninguna entidad
OfficeAssignment relacionada.

En el archivo Views/Instructors/Index.cshtml, después del elemento de tabla de cierre (situado al final del
archivo), agregue el código siguiente. Este código muestra una lista de cursos relacionados con un instructor
cuando se selecciona un instructor.

@if (Model.Courses != null)


{
<h3>Courses Taught by Selected Instructor</h3>
<table class="table">
<tr>
<th></th>
<th>Number</th>
<th>Title</th>
<th>Department</th>
</tr>

@foreach (var item in Model.Courses)


{
string selectedRow = "";
if (item.CourseID == (int?)ViewData["CourseID"])
{
selectedRow = "success";
}
<tr class="@selectedRow">
<td>
@Html.ActionLink("Select", "Index", new { courseID = item.CourseID })
</td>
<td>
@item.CourseID
</td>
<td>
@item.Title
</td>
<td>
@item.Department.Name
</td>
</tr>
}

</table>
}

Este código lee la propiedad Courses del modelo de vista para mostrar una lista de cursos. También
proporciona un hipervínculo Select que envía el identificador del curso seleccionado al método de acción
Index .

Actualice la página y seleccione un instructor. Ahora verá una cuadrícula en la que se muestran los cursos
asignados al instructor seleccionado, y para cada curso, el nombre del departamento asignado.
Después del bloque de código que se acaba de agregar, agregue el código siguiente. Esto muestra una lista de
los estudiantes que están inscritos en un curso cuando se selecciona ese curso.

@if (Model.Enrollments != null)


{
<h3>
Students Enrolled in Selected Course
</h3>
<table class="table">
<tr>
<th>Name</th>
<th>Grade</th>
</tr>
@foreach (var item in Model.Enrollments)
{
<tr>
<td>
@item.Student.FullName
</td>
<td>
@Html.DisplayFor(modelItem => item.Grade)
</td>
</tr>
}
</table>
}

Este código lee la propiedad Enrollments del modelo de vista para mostrar una lista de los estudiantes inscritos
en el curso.
Vuelva a actualizar la página y seleccione un instructor. Después, seleccione un curso para ver la lista de los
estudiantes inscritos y sus calificaciones.

Carga explícita
Cuando se recuperó la lista de instructores en InstructorsController.cs, se especificó la carga diligente de la
propiedad de navegación CourseAssignments .
Suponga que esperaba que los usuarios rara vez quisieran ver las inscripciones en un instructor y curso
seleccionados. En ese caso, es posible que quiera cargar los datos de inscripción solo si se solicitan. Para ver un
ejemplo de cómo realizar la carga explícita, reemplace el método Index con el código siguiente, que quita la
carga diligente de Enrollments y carga explícitamente esa propiedad. Los cambios de código aparecen
resaltados.
public async Task<IActionResult> Index(int? id, int? courseID)
{
var viewModel = new InstructorIndexData();
viewModel.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.OrderBy(i => i.LastName)
.ToListAsync();

if (id != null)
{
ViewData["InstructorID"] = id.Value;
Instructor instructor = viewModel.Instructors.Where(
i => i.ID == id.Value).Single();
viewModel.Courses = instructor.CourseAssignments.Select(s => s.Course);
}

if (courseID != null)
{
ViewData["CourseID"] = courseID.Value;
var selectedCourse = viewModel.Courses.Where(x => x.CourseID == courseID).Single();
await _context.Entry(selectedCourse).Collection(x => x.Enrollments).LoadAsync();
foreach (Enrollment enrollment in selectedCourse.Enrollments)
{
await _context.Entry(enrollment).Reference(x => x.Student).LoadAsync();
}
viewModel.Enrollments = selectedCourse.Enrollments;
}

return View(viewModel);
}

El nuevo código quita las llamadas al método ThenInclude para los datos de inscripción del código que recupera
las entidades Instructor. Si se seleccionan un instructor y un curso, el código resaltado recupera las entidades
Enrollment para el curso seleccionado y las entidades Student de cada inscripción.
Ejecute la aplicación, vaya a la página de índice de instructores ahora y no verá ninguna diferencia en lo que se
muestra en la página, aunque haya cambiado la forma en que se recuperan los datos.

Resumen
Ha usado la carga diligente con una consulta y con varias para leer datos relacionados en las propiedades de
navegación. En el siguiente tutorial, obtendrá información sobre cómo actualizar datos relacionados.

A N T E R IO R S IG U IE N T E
ASP.NET Core MVC con EF Core: Actualización de
datos relacionados (7 de 10)
25/06/2018 • 31 minutes to read • Edit Online

Por Tom Dykstra y Rick Anderson


En la aplicación web de ejemplo Contoso University se muestra cómo crear aplicaciones web de ASP.NET Core
MVC con Entity Framework Core y Visual Studio. Para obtener información sobre la serie de tutoriales, consulte
el primer tutorial de la serie.
En el tutorial anterior, mostró los datos relacionados; en este tutorial, actualizará los datos relacionados mediante
la actualización de campos de clave externa y las propiedades de navegación.
En las ilustraciones siguientes se muestran algunas de las páginas con las que va a trabajar.
Personalizar las páginas Create y Edit de Courses
Cuando se crea una entidad de curso, debe tener una relación con un departamento existente. Para facilitar esto,
el código con scaffolding incluye métodos de controlador y vistas de Create y Edit que incluyen una lista
desplegable para seleccionar el departamento. La lista desplegable establece la propiedad de clave externa de
Course.DepartmentID , y eso es todo lo que necesita de Entity Framework para cargar la propiedad de navegación
de Department con la entidad Department adecuada. Podrá usar el código con scaffolding, pero cámbielo
ligeramente para agregar el control de errores y ordenar la lista desplegable.
En CoursesController.cs, elimine los cuatro métodos de creación y edición, y reemplácelos con el código siguiente:

public IActionResult Create()


{
PopulateDepartmentsDropDownList();
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("CourseID,Credits,DepartmentID,Title")] Course course)
{
if (ModelState.IsValid)
{
_context.Add(course);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
PopulateDepartmentsDropDownList(course.DepartmentID);
return View(course);
}

public async Task<IActionResult> Edit(int? id)


{
if (id == null)
{
return NotFound();
}

var course = await _context.Courses


.AsNoTracking()
.SingleOrDefaultAsync(m => m.CourseID == id);
if (course == null)
{
return NotFound();
}
PopulateDepartmentsDropDownList(course.DepartmentID);
return View(course);
}
[HttpPost, ActionName("Edit")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> EditPost(int? id)
{
if (id == null)
{
return NotFound();
}

var courseToUpdate = await _context.Courses


.SingleOrDefaultAsync(c => c.CourseID == id);

if (await TryUpdateModelAsync<Course>(courseToUpdate,
"",
c => c.Credits, c => c.DepartmentID, c => c.Title))
{
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateException /* ex */)
{
//Log the error (uncomment ex variable name and write a log.)
ModelState.AddModelError("", "Unable to save changes. " +
"Try again, and if the problem persists, " +
"see your system administrator.");
}
return RedirectToAction(nameof(Index));
}
PopulateDepartmentsDropDownList(courseToUpdate.DepartmentID);
return View(courseToUpdate);
}

Después del método HttpPost de Edit , cree un método que cargue la información de departamento para la lista
desplegable.

private void PopulateDepartmentsDropDownList(object selectedDepartment = null)


{
var departmentsQuery = from d in _context.Departments
orderby d.Name
select d;
ViewBag.DepartmentID = new SelectList(departmentsQuery.AsNoTracking(), "DepartmentID", "Name",
selectedDepartment);
}

El método PopulateDepartmentsDropDownList obtiene una lista de todos los departamentos ordenados por
nombre, crea una colección SelectList para obtener una lista desplegable y pasa la colección a la vista en
ViewBag . El método acepta el parámetro opcional selectedDepartment , que permite al código que realiza la
llamada especificar el elemento que se seleccionará cuando se procese la lista desplegable. La vista pasará el
nombre "DepartmentID" a la aplicación auxiliar de etiquetas <select> , y luego la aplicación auxiliar sabe que
puede buscar en el objeto ViewBag una SelectList denominada "DepartmentID".
El método Create de HttpGet llama al método PopulateDepartmentsDropDownList sin configurar el elemento
seleccionado, ya que el departamento todavía no está establecido para un nuevo curso:

public IActionResult Create()


{
PopulateDepartmentsDropDownList();
return View();
}
El método Edit de HttpGet establece el elemento seleccionado, basándose en el identificador del departamento
que ya está asignado a la línea que se está editando:

public async Task<IActionResult> Edit(int? id)


{
if (id == null)
{
return NotFound();
}

var course = await _context.Courses


.AsNoTracking()
.SingleOrDefaultAsync(m => m.CourseID == id);
if (course == null)
{
return NotFound();
}
PopulateDepartmentsDropDownList(course.DepartmentID);
return View(course);
}

Los métodos HttpPost para Create y Edit también incluyen código que configura el elemento seleccionado
cuando vuelven a mostrar la página después de un error. Esto garantiza que, cuando vuelve a aparecer la página
para mostrar el mensaje de error, el departamento que se haya seleccionado permanece seleccionado.
Agregar AsNoTracking a los métodos Details y Delete
Para optimizar el rendimiento de las páginas Course Details y Delete, agregue llamadas AsNoTracking en los
métodos Details y Delete de HttpGet.

public async Task<IActionResult> Details(int? id)


{
if (id == null)
{
return NotFound();
}

var course = await _context.Courses


.Include(c => c.Department)
.AsNoTracking()
.SingleOrDefaultAsync(m => m.CourseID == id);
if (course == null)
{
return NotFound();
}

return View(course);
}
public async Task<IActionResult> Delete(int? id)
{
if (id == null)
{
return NotFound();
}

var course = await _context.Courses


.Include(c => c.Department)
.AsNoTracking()
.SingleOrDefaultAsync(m => m.CourseID == id);
if (course == null)
{
return NotFound();
}

return View(course);
}

Modificar las vistas de Course


En Views/Courses/Create.cshtml, agregue una opción "Select Department" a la lista desplegable Department,
cambie el título de DepartmentID a Department y agregue un mensaje de validación.

<div class="form-group">
<label asp-for="Department" class="control-label"></label>
<select asp-for="DepartmentID" class="form-control" asp-items="ViewBag.DepartmentID">
<option value="">-- Select Department --</option>
</select>
<span asp-validation-for="DepartmentID" class="text-danger" />

En Views/Courses/Edit.cshtml, realice el mismo cambio que acaba de hacer en Create.cshtml en el campo


Department.
También en Views/Courses/Edit.cshtml, agregue un campo de número de curso antes del campo Title. Dado que
el número de curso es la clave principal, esta se muestra, pero no se puede cambiar.

<div class="form-group">
<label asp-for="CourseID" class="control-label"></label>
<div>@Html.DisplayFor(model => model.CourseID)</div>
</div>

Ya hay un campo oculto ( <input type="hidden"> ) para el número de curso en la vista Edit. Agregar una aplicación
auxiliar de etiquetas <label> no elimina la necesidad de un campo oculto, porque no hace que el número de
curso se incluya en los datos enviados cuando el usuario hace clic en Save en la página Edit.
En Views/Courses/Delete.cshtml, agregue un campo de número de curso en la parte superior y cambie el
identificador del departamento por el nombre del departamento.
@model ContosoUniversity.Models.Course

@{
ViewData["Title"] = "Delete";
}

<h2>Delete</h2>

<h3>Are you sure you want to delete this?</h3>


<div>
<h4>Course</h4>
<hr />
<dl class="dl-horizontal">
<dt>
@Html.DisplayNameFor(model => model.CourseID)
</dt>
<dd>
@Html.DisplayFor(model => model.CourseID)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Title)
</dt>
<dd>
@Html.DisplayFor(model => model.Title)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Credits)
</dt>
<dd>
@Html.DisplayFor(model => model.Credits)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Department)
</dt>
<dd>
@Html.DisplayFor(model => model.Department.Name)
</dd>
</dl>

<form asp-action="Delete">
<div class="form-actions no-color">
<input type="submit" value="Delete" class="btn btn-default" /> |
<a asp-action="Index">Back to List</a>
</div>
</form>
</div>

En Views/Courses/Details.cshtml, realice el mismo cambio que acaba de hacer en Delete.cshtml.


Probar las páginas Course
Ejecute la aplicación, seleccione la pestaña Courses, haga clic en Create New y escriba los datos del curso nuevo:
Haga clic en Crear. Se muestra la página de índice de cursos con el nuevo curso agregado a la lista. El nombre de
departamento de la lista de páginas de índice proviene de la propiedad de navegación, que muestra que la
relación se estableció correctamente.
Haga clic en Edit en un curso en la página de índice de cursos.
Cambie los datos en la página y haga clic en Save. Se muestra la página de índice de cursos con los datos del
curso actualizados.

Agregar una página Edit para Instructors


Al editar un registro de instructor, necesita poder actualizar la asignación de la oficina del instructor. La entidad
Instructor tiene una relación de uno a cero o uno con la entidad OfficeAssignment, lo que significa que el código
tiene que controlar las situaciones siguientes:
Si el usuario borra la asignación de oficina y esta tenía originalmente un valor, elimine la entidad
OfficeAssignment.
Si el usuario escribe un valor de asignación de oficina y originalmente estaba vacío, cree una entidad
OfficeAssignment.
Si el usuario cambia el valor de una asignación de oficina, cambie el valor en una entidad
OfficeAssignment existente.
Actualizar el controlador de Instructors
En InstructorsController.cs, cambie el código en el método Edit de HttpGet para que cargue la propiedad de
navegación OfficeAssignment de la entidad Instructor y llame a AsNoTracking :
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
return NotFound();
}

var instructor = await _context.Instructors


.Include(i => i.OfficeAssignment)
.AsNoTracking()
.SingleOrDefaultAsync(m => m.ID == id);
if (instructor == null)
{
return NotFound();
}
return View(instructor);
}

Reemplace el método Edit de HttpPost con el siguiente código para controlar las actualizaciones de
asignaciones de oficina:

[HttpPost, ActionName("Edit")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> EditPost(int? id)
{
if (id == null)
{
return NotFound();
}

var instructorToUpdate = await _context.Instructors


.Include(i => i.OfficeAssignment)
.SingleOrDefaultAsync(s => s.ID == id);

if (await TryUpdateModelAsync<Instructor>(
instructorToUpdate,
"",
i => i.FirstMidName, i => i.LastName, i => i.HireDate, i => i.OfficeAssignment))
{
if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment?.Location))
{
instructorToUpdate.OfficeAssignment = null;
}
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateException /* ex */)
{
//Log the error (uncomment ex variable name and write a log.)
ModelState.AddModelError("", "Unable to save changes. " +
"Try again, and if the problem persists, " +
"see your system administrator.");
}
return RedirectToAction(nameof(Index));
}
return View(instructorToUpdate);
}

El código realiza lo siguiente:


Cambia el nombre del método a EditPost porque la firma ahora es la misma que el método Edit de
HttpGet (el atributo ActionName especifica que la dirección URL de /Edit/ aún está en uso).
Obtiene la entidad Instructor actual de la base de datos mediante la carga diligente de la propiedad de
navegación OfficeAssignment . Esto es lo mismo que hizo en el método Edit de HttpGet.
Actualiza la entidad Instructor recuperada con valores del enlazador de modelos. La sobrecarga de
TryUpdateModel le permite crear una lista de permitidos con las propiedades que quiera incluir. Esto evita
el registro excesivo, como se explica en el segundo tutorial.

if (await TryUpdateModelAsync<Instructor>(
instructorToUpdate,
"",
i => i.FirstMidName, i => i.LastName, i => i.HireDate, i => i.OfficeAssignment))

Si la ubicación de la oficina está en blanco, establece la propiedad Instructor.OfficeAssignment en NULL


para que se elimine la fila relacionada en la tabla OfficeAssignment.

if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment?.Location))
{
instructorToUpdate.OfficeAssignment = null;
}

Guarda los cambios en la base de datos.


Actualizar la vista de Edit de Instructor
En Views/Instructors/Edit.cshtml, agregue un nuevo campo para editar la ubicación de la oficina, al final antes del
botón Save:

<div class="form-group">
<label asp-for="OfficeAssignment.Location" class="control-label"></label>
<input asp-for="OfficeAssignment.Location" class="form-control" />
<span asp-validation-for="OfficeAssignment.Location" class="text-danger" />
</div>

Ejecute la aplicación, seleccione la pestaña Instructors y, después, haga clic en Edit en un instructor. Cambie el
valor de Office Location y haga clic en Save.
Agregar asignaciones de cursos a la página Edit de los instructores
Los instructores pueden impartir cualquier número de cursos. Ahora mejorará la página Edit Instructor al agregar
la capacidad de cambiar las asignaciones de cursos mediante un grupo de casillas, tal y como se muestra en la
siguiente captura de pantalla:
La relación entre las entidades Course e Instructor es varios a varios. Para agregar y eliminar relaciones, agregue
y quite entidades del conjunto de entidades combinadas CourseAssignments.
La interfaz de usuario que le permite cambiar los cursos a los que está asignado un instructor es un grupo de
casillas. Se muestra una casilla para cada curso en la base de datos y se seleccionan aquellos a los que está
asignado actualmente el instructor. El usuario puede activar o desactivar las casillas para cambiar las asignaciones
de cursos. Si el número de cursos fuera mucho mayor, probablemente tendría que usar un método diferente de
presentar los datos en la vista, pero usaría el mismo método de manipulación de una entidad de combinación
para crear o eliminar relaciones.
Actualizar el controlador de Instructors
Para proporcionar datos a la vista de la lista de casillas, deberá usar una clase de modelo de vista.
Cree AssignedCourseData.cs en la carpeta SchoolViewModels y reemplace el código existente con el código
siguiente:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Models.SchoolViewModels
{
public class AssignedCourseData
{
public int CourseID { get; set; }
public string Title { get; set; }
public bool Assigned { get; set; }
}
}

En InstructorsController.cs, reemplace el método Edit de HttpGet por el código siguiente. Los cambios aparecen
resaltados.

public async Task<IActionResult> Edit(int? id)


{
if (id == null)
{
return NotFound();
}

var instructor = await _context.Instructors


.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments).ThenInclude(i => i.Course)
.AsNoTracking()
.SingleOrDefaultAsync(m => m.ID == id);
if (instructor == null)
{
return NotFound();
}
PopulateAssignedCourseData(instructor);
return View(instructor);
}

private void PopulateAssignedCourseData(Instructor instructor)


{
var allCourses = _context.Courses;
var instructorCourses = new HashSet<int>(instructor.CourseAssignments.Select(c => c.CourseID));
var viewModel = new List<AssignedCourseData>();
foreach (var course in allCourses)
{
viewModel.Add(new AssignedCourseData
{
CourseID = course.CourseID,
Title = course.Title,
Assigned = instructorCourses.Contains(course.CourseID)
});
}
ViewData["Courses"] = viewModel;
}

El código agrega carga diligente para la propiedad de navegación Courses y llama al método
PopulateAssignedCourseData nuevo para proporcionar información de la matriz de casilla mediante la clase de
modelo de vista AssignedCourseData .
El código en el método PopulateAssignedCourseData lee a través de todas las entidades Course para cargar una
lista de cursos mediante la clase de modelo de vista. Para cada curso, el código comprueba si existe el curso en la
propiedad de navegación Courses del instructor. Para crear una búsqueda eficaz al comprobar si un curso está
asignado al instructor, los cursos asignados a él se colocan en una colección HashSet . La propiedad Assigned
está establecida en true para los cursos a los que está asignado el instructor. La vista usará esta propiedad para
determinar qué casilla debe mostrarse como seleccionada. Por último, la lista se pasa a la vista en ViewData .
A continuación, agregue el código que se ejecuta cuando el usuario hace clic en Save. Reemplace el método
EditPost con el siguiente código y agregue un nuevo método que actualiza la propiedad de navegación Courses
de la entidad Instructor.

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int? id, string[] selectedCourses)
{
if (id == null)
{
return NotFound();
}

var instructorToUpdate = await _context.Instructors


.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.SingleOrDefaultAsync(m => m.ID == id);

if (await TryUpdateModelAsync<Instructor>(
instructorToUpdate,
"",
i => i.FirstMidName, i => i.LastName, i => i.HireDate, i => i.OfficeAssignment))
{
if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment?.Location))
{
instructorToUpdate.OfficeAssignment = null;
}
UpdateInstructorCourses(selectedCourses, instructorToUpdate);
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateException /* ex */)
{
//Log the error (uncomment ex variable name and write a log.)
ModelState.AddModelError("", "Unable to save changes. " +
"Try again, and if the problem persists, " +
"see your system administrator.");
}
return RedirectToAction(nameof(Index));
}
UpdateInstructorCourses(selectedCourses, instructorToUpdate);
PopulateAssignedCourseData(instructorToUpdate);
return View(instructorToUpdate);
}
private void UpdateInstructorCourses(string[] selectedCourses, Instructor instructorToUpdate)
{
if (selectedCourses == null)
{
instructorToUpdate.CourseAssignments = new List<CourseAssignment>();
return;
}

var selectedCoursesHS = new HashSet<string>(selectedCourses);


var instructorCourses = new HashSet<int>
(instructorToUpdate.CourseAssignments.Select(c => c.Course.CourseID));
foreach (var course in _context.Courses)
{
if (selectedCoursesHS.Contains(course.CourseID.ToString()))
{
if (!instructorCourses.Contains(course.CourseID))
{
instructorToUpdate.CourseAssignments.Add(new CourseAssignment { InstructorID =
instructorToUpdate.ID, CourseID = course.CourseID });
}
}
else
{

if (instructorCourses.Contains(course.CourseID))
{
CourseAssignment courseToRemove = instructorToUpdate.CourseAssignments.SingleOrDefault(i =>
i.CourseID == course.CourseID);
_context.Remove(courseToRemove);
}
}
}
}

La firma del método ahora es diferente del método Edit de HttpGet, por lo que el nombre del método cambia
de EditPost a Edit .
Puesto que la vista no tiene una colección de entidades Course, el enlazador de modelos no puede actualizar
automáticamente la propiedad de navegación CourseAssignments . En lugar de usar el enlazador de modelos para
actualizar la propiedad de navegación CourseAssignments , lo hace en el nuevo método UpdateInstructorCourses .
Por lo tanto, tendrá que excluir la propiedad CourseAssignments del enlace de modelos. Esto no requiere ningún
cambio en el código que llama a TryUpdateModel porque está usando la sobrecarga de la creación de listas de
permitidos y CourseAssignments no se encuentra en la lista de inclusión.
Si no se ha seleccionado ninguna casilla, el código en UpdateInstructorCourses inicializa la propiedad de
navegación CourseAssignments con una colección vacía y devuelve:
private void UpdateInstructorCourses(string[] selectedCourses, Instructor instructorToUpdate)
{
if (selectedCourses == null)
{
instructorToUpdate.CourseAssignments = new List<CourseAssignment>();
return;
}

var selectedCoursesHS = new HashSet<string>(selectedCourses);


var instructorCourses = new HashSet<int>
(instructorToUpdate.CourseAssignments.Select(c => c.Course.CourseID));
foreach (var course in _context.Courses)
{
if (selectedCoursesHS.Contains(course.CourseID.ToString()))
{
if (!instructorCourses.Contains(course.CourseID))
{
instructorToUpdate.CourseAssignments.Add(new CourseAssignment { InstructorID =
instructorToUpdate.ID, CourseID = course.CourseID });
}
}
else
{

if (instructorCourses.Contains(course.CourseID))
{
CourseAssignment courseToRemove = instructorToUpdate.CourseAssignments.SingleOrDefault(i =>
i.CourseID == course.CourseID);
_context.Remove(courseToRemove);
}
}
}
}

A continuación, el código recorre en bucle todos los cursos de la base de datos y coteja los que están asignados
actualmente al instructor frente a los que se han seleccionado en la vista. Para facilitar las búsquedas eficaces,
estas dos últimas colecciones se almacenan en objetos HashSet .
Si se ha activado la casilla para un curso pero este no se encuentra en la propiedad de navegación
Instructor.CourseAssignments , el curso se agrega a la colección en la propiedad de navegación.
private void UpdateInstructorCourses(string[] selectedCourses, Instructor instructorToUpdate)
{
if (selectedCourses == null)
{
instructorToUpdate.CourseAssignments = new List<CourseAssignment>();
return;
}

var selectedCoursesHS = new HashSet<string>(selectedCourses);


var instructorCourses = new HashSet<int>
(instructorToUpdate.CourseAssignments.Select(c => c.Course.CourseID));
foreach (var course in _context.Courses)
{
if (selectedCoursesHS.Contains(course.CourseID.ToString()))
{
if (!instructorCourses.Contains(course.CourseID))
{
instructorToUpdate.CourseAssignments.Add(new CourseAssignment { InstructorID =
instructorToUpdate.ID, CourseID = course.CourseID });
}
}
else
{

if (instructorCourses.Contains(course.CourseID))
{
CourseAssignment courseToRemove = instructorToUpdate.CourseAssignments.SingleOrDefault(i =>
i.CourseID == course.CourseID);
_context.Remove(courseToRemove);
}
}
}
}

Si no se ha activado la casilla para un curso pero este se encuentra en la propiedad de navegación


Instructor.CourseAssignments , el curso se quita de la colección en la propiedad de navegación.
private void UpdateInstructorCourses(string[] selectedCourses, Instructor instructorToUpdate)
{
if (selectedCourses == null)
{
instructorToUpdate.CourseAssignments = new List<CourseAssignment>();
return;
}

var selectedCoursesHS = new HashSet<string>(selectedCourses);


var instructorCourses = new HashSet<int>
(instructorToUpdate.CourseAssignments.Select(c => c.Course.CourseID));
foreach (var course in _context.Courses)
{
if (selectedCoursesHS.Contains(course.CourseID.ToString()))
{
if (!instructorCourses.Contains(course.CourseID))
{
instructorToUpdate.CourseAssignments.Add(new CourseAssignment { InstructorID =
instructorToUpdate.ID, CourseID = course.CourseID });
}
}
else
{

if (instructorCourses.Contains(course.CourseID))
{
CourseAssignment courseToRemove = instructorToUpdate.CourseAssignments.SingleOrDefault(i =>
i.CourseID == course.CourseID);
_context.Remove(courseToRemove);
}
}
}
}

Actualizar las vistas de Instructor


En Views/Instructors/Edit.cshtml, agregue un campo Courses con una matriz de casillas al agregar el siguiente
código inmediatamente después de los elementos div del campo Office y antes del elemento div del botón
Save.

NOTE
Al pegar el código en Visual Studio, se cambiarán los saltos de línea de tal forma que el código se interrumpe. Presione
Ctrl+Z una vez para deshacer el formato automático. Esto corregirá los saltos de línea para que se muestren como se ven
aquí. No es necesario que la sangría sea perfecta, pero las líneas @</tr><tr> , @:<td> , @:</td> y @:</tr> deben estar
en una única línea tal y como se muestra, de lo contrario, obtendrá un error en tiempo de ejecución. Con el bloque de
código nuevo seleccionado, presione tres veces la tecla TAB para alinearlo con el código existente. Puede comprobar el
estado de este problema aquí.
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<table>
<tr>
@{
int cnt = 0;
List<ContosoUniversity.Models.SchoolViewModels.AssignedCourseData> courses =
ViewBag.Courses;

foreach (var course in courses)


{
if (cnt++ % 3 == 0)
{
@:</tr><tr>
}
@:<td>
<input type="checkbox"
name="selectedCourses"
value="@course.CourseID"
@(Html.Raw(course.Assigned ? "checked=\"checked\"" : "")) />
@course.CourseID @: @course.Title
@:</td>
}
@:</tr>
}
</table>
</div>
</div>

Este código crea una tabla HTML que tiene tres columnas. En cada columna hay una casilla seguida de un título
que está formado por el número y el título del curso. Todas las casillas tienen el mismo nombre
("selectedCourses"), que informa al enlazador de modelos que se deben tratar como un grupo. El atributo de
valor de cada casilla se establece en el valor de CourseID . Cuando se envía la página, el enlazador de modelos
pasa una matriz al controlador formada solo por los valores CourseID de las casillas activadas.
Cuando las casillas se representan inicialmente, aquellas que son para cursos asignados al instructor tienen
atributos seleccionados, lo que las selecciona (las muestra activadas).
Ejecute la aplicación, seleccione la pestaña Instructors y haga clic en Edit en un instructor para ver la página
Edit.
Cambie algunas asignaciones de cursos y haga clic en Save. Los cambios que haga se reflejan en la página de
índice.

NOTE
El enfoque que se aplica aquí para modificar datos de los cursos del instructor funciona bien cuando hay un número
limitado de cursos. Para las colecciones que son mucho más grandes, se necesitaría una interfaz de usuario y un método de
actualización diferentes.

Actualizar la página Delete


En InstructorsController.cs, elimine el método DeleteConfirmed e inserte el siguiente código en su lugar.
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
Instructor instructor = await _context.Instructors
.Include(i => i.CourseAssignments)
.SingleAsync(i => i.ID == id);

var departments = await _context.Departments


.Where(d => d.InstructorID == id)
.ToListAsync();
departments.ForEach(d => d.InstructorID = null);

_context.Instructors.Remove(instructor);

await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}

Este código realiza los cambios siguientes:


Hace la carga diligente para la propiedad de navegación CourseAssignments . Tiene que incluir esto o EF no
conocerá las entidades CourseAssignment relacionadas y no las eliminará. Para evitar la necesidad de leerlos
aquí, puede configurar la eliminación en cascada en la base de datos.
Si el instructor que se va a eliminar está asignado como administrador de cualquiera de los
departamentos, quita la asignación de instructor de esos departamentos.

Agregar la ubicación de la oficina y cursos a la página Create


En InstructorsController.cs, elimine los métodos Create de HttpGet y HttpPost y, después, agregue el código
siguiente en su lugar:
public IActionResult Create()
{
var instructor = new Instructor();
instructor.CourseAssignments = new List<CourseAssignment>();
PopulateAssignedCourseData(instructor);
return View();
}

// POST: Instructors/Create
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("FirstMidName,HireDate,LastName,OfficeAssignment")] Instructor
instructor, string[] selectedCourses)
{
if (selectedCourses != null)
{
instructor.CourseAssignments = new List<CourseAssignment>();
foreach (var course in selectedCourses)
{
var courseToAdd = new CourseAssignment { InstructorID = instructor.ID, CourseID =
int.Parse(course) };
instructor.CourseAssignments.Add(courseToAdd);
}
}
if (ModelState.IsValid)
{
_context.Add(instructor);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
PopulateAssignedCourseData(instructor);
return View(instructor);
}

Este código es similar a lo que ha visto para los métodos Edit , excepto que no hay cursos seleccionados
inicialmente. El método Create de HttpGet no llama al método PopulateAssignedCourseData porque pueda haber
cursos seleccionados sino para proporcionar una colección vacía para el bucle foreach en la vista (en caso
contrario, el código de vista podría producir una excepción de referencia nula).
El método Create de HttpPost agrega cada curso seleccionado a la propiedad de navegación CourseAssignments
antes de comprobar si hay errores de validación y agrega el instructor nuevo a la base de datos. Los cursos se
agregan incluso si hay errores de modelo, por lo que cuando hay errores del modelo (por ejemplo, el usuario
escribió una fecha no válida) y se vuelve a abrir la página con un mensaje de error, las selecciones de cursos que
se habían realizado se restauran todas automáticamente.
Tenga en cuenta que, para poder agregar cursos a la propiedad de navegación CourseAssignments , debe inicializar
la propiedad como una colección vacía:

instructor.CourseAssignments = new List<CourseAssignment>();

Como alternativa a hacerlo en el código de control, podría hacerlo en el modelo de Instructor cambiando el
captador de propiedad para que cree automáticamente la colección si no existe, como se muestra en el ejemplo
siguiente:
private ICollection<CourseAssignment> _courseAssignments;
public ICollection<CourseAssignment> CourseAssignments
{
get
{
return _courseAssignments ?? (_courseAssignments = new List<CourseAssignment>());
}
set
{
_courseAssignments = value;
}
}

Si modifica la propiedad CourseAssignments de esta manera, puede quitar el código de inicialización de propiedad
explícito del controlador.
En Views/Instructor/Create.cshtml, agregue un cuadro de texto de la ubicación de la oficina y casillas para cursos
antes del botón Submit. Al igual que en el caso de la página Edit, corrija el formato si Visual Studio vuelve a
aplicar formato al código al pegarlo.

<div class="form-group">
<label asp-for="OfficeAssignment.Location" class="control-label"></label>
<input asp-for="OfficeAssignment.Location" class="form-control" />
<span asp-validation-for="OfficeAssignment.Location" class="text-danger" />
</div>

<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<table>
<tr>
@{
int cnt = 0;
List<ContosoUniversity.Models.SchoolViewModels.AssignedCourseData> courses =
ViewBag.Courses;

foreach (var course in courses)


{
if (cnt++ % 3 == 0)
{
@:</tr><tr>
}
@:<td>
<input type="checkbox"
name="selectedCourses"
value="@course.CourseID"
@(Html.Raw(course.Assigned ? "checked=\"checked\"" : "")) />
@course.CourseID @: @course.Title
@:</td>
}
@:</tr>
}
</table>
</div>
</div>

Pruebe a ejecutar la aplicación y crear un instructor.

Control de transacciones
Como se explicó en el tutorial de CRUD, Entity Framework implementa las transacciones de manera implícita.
Para escenarios donde se necesita más control, por ejemplo, si se quieren incluir operaciones realizadas fuera de
Entity Framework en una transacción, vea Transacciones.
Resumen
Ahora ha completado la introducción para trabajar con datos relacionados. En el siguiente tutorial verá cómo
tratar los conflictos de simultaneidad.

A N T E R IO R S IG U IE N T E
ASP.NET Core MVC con EF Core: Simultaneidad (8
de 10)
25/06/2018 • 33 minutes to read • Edit Online

Por Tom Dykstra y Rick Anderson


En la aplicación web de ejemplo Contoso University se muestra cómo crear aplicaciones web de ASP.NET Core
MVC con Entity Framework Core y Visual Studio. Para obtener información sobre la serie de tutoriales, consulte
el primer tutorial de la serie.
En los tutoriales anteriores, aprendió a actualizar los datos. Este tutorial muestra cómo tratar los conflictos
cuando varios usuarios actualizan la misma entidad al mismo tiempo.
Podrá crear páginas web que funcionan con la entidad Department y controlan los errores de simultaneidad. Las
siguientes ilustraciones muestran las páginas Edit y Delete, incluidos algunos mensajes que se muestran si se
produce un conflicto de simultaneidad.
Conflictos de simultaneidad
Los conflictos de simultaneidad ocurren cuando un usuario muestra los datos de una entidad para editarlos y,
después, otro usuario actualiza los datos de la misma entidad antes de que el primer cambio del usuario se
escriba en la base de datos. Si no habilita la detección de este tipo de conflictos, quien actualice la base de datos
en último lugar sobrescribe los cambios del otro usuario. En muchas aplicaciones, el riesgo es aceptable: si hay
pocos usuarios o pocas actualizaciones, o si no es realmente importante si se sobrescriben algunos cambios, el
costo de programación para la simultaneidad puede superar el beneficio obtenido. En ese caso, no tendrá que
configurar la aplicación para que controle los conflictos de simultaneidad.
Simultaneidad pesimista (bloqueo )
Si la aplicación necesita evitar la pérdida accidental de datos en casos de simultaneidad, una manera de hacerlo es
usar los bloqueos de base de datos. Esto se denomina simultaneidad pesimista. Por ejemplo, antes de leer una fila
de una base de datos, solicita un bloqueo de solo lectura o para acceso de actualización. Si bloquea una fila para
acceso de actualización, no se permite que ningún otro usuario bloquee la fila como solo lectura o para acceso de
actualización, porque recibirían una copia de los datos que se están modificando. Si bloquea una fila para acceso
de solo lectura, otras personas también pueden bloquearla para acceso de solo lectura pero no para actualización.
Administrar los bloqueos tiene desventajas. Puede ser bastante complicado de programar. Se necesita un número
significativo de recursos de administración de base de datos, y puede provocar problemas de rendimiento a
medida que aumenta el número de usuarios de una aplicación. Por estos motivos, no todos los sistemas de
administración de bases de datos admiten la simultaneidad pesimista. Entity Framework Core no proporciona
ninguna compatibilidad integrada para ello y en este tutorial no se muestra cómo implementarla.
Simultaneidad optimista
La alternativa a la simultaneidad pesimista es la simultaneidad optimista. La simultaneidad optimista implica
permitir que se produzcan conflictos de simultaneidad y reaccionar correctamente si ocurren. Por ejemplo, Jane
visita la página Edit Department y cambia la cantidad de Budget para el departamento de inglés de 350.000,00 a
0,00 USD.

Antes de que Jane haga clic en Save, John visita la misma página y cambia el campo Start Date de 9/1/2007 a
9/1/2013.
Jane hace clic en Save primero y ve su cambio cuando el explorador vuelve a la página de índice.

Entonces, John hace clic en Save en una página Edit que sigue mostrando un presupuesto de 350.000,00 USD.
Lo que sucede después viene determinado por cómo controla los conflictos de simultaneidad.
Algunas de las opciones se exponen a continuación:
Puede realizar un seguimiento de la propiedad que ha modificado un usuario y actualizar solo las
columnas correspondientes de la base de datos.
En el escenario de ejemplo, no se perdería ningún dato porque los dos usuarios actualizaron diferentes
propiedades. La próxima vez que un usuario examine el departamento de inglés, verá los cambios tanto de
Jane como de John: una fecha de inicio de 9/1/2013 y un presupuesto de cero dólares. Este método de
actualización puede reducir el número de conflictos que pueden dar lugar a una pérdida de datos, pero no
puede evitar la pérdida de datos si se realizan cambios paralelos a la misma propiedad de una entidad. Si
Entity Framework funciona de esta manera o no, depende de cómo implemente el código de actualización.
A menudo no resulta práctico en una aplicación web, porque puede requerir mantener grandes cantidades
de estado con el fin de realizar un seguimiento de todos los valores de propiedad originales de una
entidad, así como los valores nuevos. Mantener grandes cantidades de estado puede afectar al
rendimiento de la aplicación porque requiere recursos del servidor o se deben incluir en la propia página
web (por ejemplo, en campos ocultos) o en una cookie.
Puede permitir que los cambios de John sobrescriban los cambios de Jane.
La próxima vez que un usuario examine el departamento de inglés, verá 9/1/2013 y el valor de 350.000,00
USD restaurado. Esto se denomina un escenario de Prevalece el cliente o Prevalece el último. (Todos los
valores del cliente tienen prioridad sobre lo que aparece en el almacén de datos). Como se mencionó en la
introducción de esta sección, si no hace ninguna codificación para el control de la simultaneidad, se
realizará automáticamente.
Puede evitar que el cambio de John se actualice en la base de datos.
Por lo general, debería mostrar un mensaje de error, mostrarle el estado actual de los datos y permitirle
volver a aplicar los cambios si quiere realizarlos. Esto se denomina un escenario de Prevalece el almacén.
(Los valores del almacén de datos tienen prioridad sobre los valores enviados por el cliente). En este
tutorial implementará el escenario de Prevalece el almacén. Este método garantiza que ningún cambio se
sobrescriba sin que se avise al usuario de lo que está sucediendo.
Detectar los conflictos de simultaneidad
Puede resolver los conflictos controlando las excepciones DbConcurrencyException que inicia Entity Framework.
Para saber cuándo se producen dichas excepciones, Entity Framework debe ser capaz de detectar conflictos. Por
lo tanto, debe configurar correctamente la base de datos y el modelo de datos. Algunas opciones para habilitar la
detección de conflictos son las siguientes:
En la tabla de la base de datos, incluya una columna de seguimiento que pueda usarse para determinar si
una fila ha cambiado. Después puede configurar Entity Framework para que incluya esa columna en la
cláusula Where de los comandos Update o Delete de SQL.
El tipo de datos de la columna de seguimiento suele ser rowversion . El valor rowversion es un número
secuencial que se incrementa cada vez que se actualiza la fila. En un comando Update o Delete, la cláusula
Where incluye el valor original de la columna de seguimiento (la versión de la fila original). Si otro usuario
ha cambiado la fila que se está actualizando, el valor en la columna rowversion es diferente del valor
original, por lo que la instrucción Update o Delete no puede encontrar la fila que se va a actualizar debido
a la cláusula Where. Cuando Entity Framework encuentra que no se ha actualizado ninguna fila mediante
el comando Update o Delete (es decir, cuando el número de filas afectadas es cero), lo interpreta como un
conflicto de simultaneidad.
Configure Entity Framework para que incluya los valores originales de cada columna de la tabla en la
cláusula Where de los comandos Update y Delete.
Como se muestra en la primera opción, si algo en la fila ha cambiado desde que se leyó por primera, la
cláusula Where no devolverá una fila para actualizar, lo cual Entity Framework interpreta como un conflicto
de simultaneidad. Para las tablas de base de datos que tienen muchas columnas, este enfoque puede dar
lugar a cláusulas Where muy grandes y puede requerir mantener grandes cantidades de estado. Tal y
como se indicó anteriormente, el mantenimiento de grandes cantidades de estado puede afectar al
rendimiento de la aplicación. Por tanto, generalmente este enfoque no se recomienda y no es el método
usado en este tutorial.
Si quiere implementar este enfoque para la simultaneidad, tendrá que marcar todas las propiedades de
clave no principal de la entidad de la que quiere realizar un seguimiento de simultaneidad agregándoles el
atributo ConcurrencyCheck . Este cambio permite que Entity Framework incluya todas las columnas en la
cláusula Where de SQL de las instrucciones Update y Delete.
En el resto de este tutorial agregará una propiedad de seguimiento rowversion para la entidad Department,
creará un controlador y vistas, y comprobará que todo funciona correctamente.

Agregar una propiedad de seguimiento a la entidad Department


En Models/Department.cs, agregue una propiedad de seguimiento denominada RowVersion:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
public class Department
{
public int DepartmentID { get; set; }

[StringLength(50, MinimumLength = 3)]


public string Name { get; set; }

[DataType(DataType.Currency)]
[Column(TypeName = "money")]
public decimal Budget { get; set; }

[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
[Display(Name = "Start Date")]
public DateTime StartDate { get; set; }

public int? InstructorID { get; set; }

[Timestamp]
public byte[] RowVersion { get; set; }

public Instructor Administrator { get; set; }


public ICollection<Course> Courses { get; set; }
}
}

El atributo Timestamp especifica que esta columna se incluirá en la cláusula Where de los comandos Update y
Delete enviados a la base de datos. El atributo se denomina Timestamp porque las versiones anteriores de SQL
Server usaban un tipo de datos timestamp antes de que la rowversion de SQL lo sustituyera por otro. El tipo
.NET de rowversion es una matriz de bytes.
Si prefiere usar la API fluida, puede usar el método IsConcurrencyToken (en Data/SchoolContext.cs) para
especificar la propiedad de seguimiento, tal como se muestra en el ejemplo siguiente:

modelBuilder.Entity<Department>()
.Property(p => p.RowVersion).IsConcurrencyToken();

Al agregar una propiedad cambió el modelo de base de datos, por lo que necesita realizar otra migración.
Guarde los cambios, compile el proyecto y, después, escriba los siguientes comandos en la ventana de comandos:

dotnet ef migrations add RowVersion


dotnet ef database update

Crear un controlador y vistas de Departments


Aplique la técnica scaffolding a un controlador y vistas de Departments como lo hizo anteriormente para
Students, Courses e Instructors.

En el archivo DepartmentsController.cs, cambie las cuatro repeticiones de "FirstMidName" a "FullName" para que
las listas desplegables del administrador del departamento contengan el nombre completo del instructor en lugar
de simplemente el apellido.

ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID", "FullName", department.InstructorID);

Actualizar la vista de índice de Departments


El motor de scaffolding creó una columna RowVersion en la vista Index, pero ese campo no debería mostrarse.
Reemplace el código de Views/Departments/Index.cshtml con el código siguiente.
@model IEnumerable<ContosoUniversity.Models.Department>

@{
ViewData["Title"] = "Departments";
}

<h2>Departments</h2>

<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Name)
</th>
<th>
@Html.DisplayNameFor(model => model.Budget)
</th>
<th>
@Html.DisplayNameFor(model => model.StartDate)
</th>
<th>
@Html.DisplayNameFor(model => model.Administrator)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Name)
</td>
<td>
@Html.DisplayFor(modelItem => item.Budget)
</td>
<td>
@Html.DisplayFor(modelItem => item.StartDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Administrator.FullName)
</td>
<td>
<a asp-action="Edit" asp-route-id="@item.DepartmentID">Edit</a> |
<a asp-action="Details" asp-route-id="@item.DepartmentID">Details</a> |
<a asp-action="Delete" asp-route-id="@item.DepartmentID">Delete</a>
</td>
</tr>
}
</tbody>
</table>

Esto cambia el encabezado por "Departments", elimina la columna RowVersion y muestra el nombre completo en
lugar del nombre del administrador.

Actualizar los métodos Edit en el controlador de Departments


En el método Edit de HttpGet y el método Details , agregue AsNoTracking . En el método Edit de HttpGet,
agregue carga diligente para el administrador.
var department = await _context.Departments
.Include(i => i.Administrator)
.AsNoTracking()
.SingleOrDefaultAsync(m => m.DepartmentID == id);

Sustituya el código existente para el método Edit de HttpPost por el siguiente código:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int? id, byte[] rowVersion)
{
if (id == null)
{
return NotFound();
}

var departmentToUpdate = await _context.Departments.Include(i => i.Administrator).SingleOrDefaultAsync(m


=> m.DepartmentID == id);

if (departmentToUpdate == null)
{
Department deletedDepartment = new Department();
await TryUpdateModelAsync(deletedDepartment);
ModelState.AddModelError(string.Empty,
"Unable to save changes. The department was deleted by another user.");
ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID", "FullName",
deletedDepartment.InstructorID);
return View(deletedDepartment);
}

_context.Entry(departmentToUpdate).Property("RowVersion").OriginalValue = rowVersion;

if (await TryUpdateModelAsync<Department>(
departmentToUpdate,
"",
s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
{
try
{
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
catch (DbUpdateConcurrencyException ex)
{
var exceptionEntry = ex.Entries.Single();
var clientValues = (Department)exceptionEntry.Entity;
var databaseEntry = exceptionEntry.GetDatabaseValues();
if (databaseEntry == null)
{
ModelState.AddModelError(string.Empty,
"Unable to save changes. The department was deleted by another user.");
}
else
{
var databaseValues = (Department)databaseEntry.ToObject();

if (databaseValues.Name != clientValues.Name)
{
ModelState.AddModelError("Name", $"Current value: {databaseValues.Name}");
}
if (databaseValues.Budget != clientValues.Budget)
{
ModelState.AddModelError("Budget", $"Current value: {databaseValues.Budget:c}");
}
if (databaseValues.StartDate != clientValues.StartDate)
{
ModelState.AddModelError("StartDate", $"Current value: {databaseValues.StartDate:d}");
}
if (databaseValues.InstructorID != clientValues.InstructorID)
{
Instructor databaseInstructor = await _context.Instructors.SingleOrDefaultAsync(i => i.ID
== databaseValues.InstructorID);
ModelState.AddModelError("InstructorID", $"Current value:
{databaseInstructor?.FullName}");
}

ModelState.AddModelError(string.Empty, "The record you attempted to edit "


+ "was modified by another user after you got the original value. The "
+ "edit operation was canceled and the current values in the database "
+ "have been displayed. If you still want to edit this record, click "
+ "the Save button again. Otherwise click the Back to List hyperlink.");
departmentToUpdate.RowVersion = (byte[])databaseValues.RowVersion;
ModelState.Remove("RowVersion");
}
}
}
ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID", "FullName",
departmentToUpdate.InstructorID);
return View(departmentToUpdate);
}

El código comienza por intentar leer el departamento que se va a actualizar. Si el método SingleOrDefaultAsync
devuelve NULL, otro usuario eliminó el departamento. En ese caso, el código usa los valores del formulario
expuesto para crear una entidad Department, por lo que puede volver a mostrarse la página Edit con un mensaje
de error. Como alternativa, no tendrá que volver a crear la entidad Department si solo muestra un mensaje de
error sin volver a mostrar los campos del departamento.
La vista almacena el valor RowVersion original en un campo oculto, y este método recibe ese valor en el
parámetro rowVersion . Antes de llamar a SaveChanges , tendrá que colocar dicho valor de propiedad RowVersion
original en la colección OriginalValues para la entidad.

_context.Entry(departmentToUpdate).Property("RowVersion").OriginalValue = rowVersion;

Cuando Entity Framework crea un comando UPDATE de SQL, ese comando incluirá una cláusula WHERE que
comprueba si hay una fila que tenga el valor RowVersion original. Si no hay ninguna fila afectada por el comando
UPDATE (ninguna fila tiene el valor RowVersion original), Entity Framework inicia una excepción
DbUpdateConcurrencyException .

El código del bloque catch de esa excepción obtiene la entidad Department afectada que tiene los valores
actualizados de la propiedad Entries del objeto de excepción.

var exceptionEntry = ex.Entries.Single();

La colección Entries contará con un solo objeto EntityEntry . Puede usar dicho objeto para obtener los nuevos
valores especificados por el usuario y los valores actuales de la base de datos.

var clientValues = (Department)exceptionEntry.Entity;


var databaseEntry = exceptionEntry.GetDatabaseValues();

El código agrega un mensaje de error personalizado para cada columna que tenga valores de base de datos
diferentes de lo que el usuario especificó en la página Edit (aquí solo se muestra un campo por razones de
brevedad).
var databaseValues = (Department)databaseEntry.ToObject();

if (databaseValues.Name != clientValues.Name)
{
ModelState.AddModelError("Name", $"Current value: {databaseValues.Name}");

Por último, el código establece el valor RowVersion de departmentToUpdate para el nuevo valor recuperado de la
base de datos. Este nuevo valor RowVersion se almacenará en el campo oculto cuando se vuelva a mostrar la
página Edit y, la próxima vez que el usuario haga clic en Save, solo se detectarán los errores de simultaneidad
que se produzcan desde que se vuelva a mostrar la página Edit.

departmentToUpdate.RowVersion = (byte[])databaseValues.RowVersion;
ModelState.Remove("RowVersion");

La instrucción ModelState.Remove es necesaria porque ModelState tiene el valor RowVersion antiguo. En la vista,
el valor ModelState de un campo tiene prioridad sobre los valores de propiedad de modelo cuando ambos están
presentes.

Actualizar la vista Department Edit


En Views/Departments/Edit.cshtml, realice los cambios siguientes:
Agregue un campo oculto para guardar el valor de propiedad RowVersion , inmediatamente después de un
campo oculto para la propiedad DepartmentID .
Agregue una opción "Select Administrator" a la lista desplegable.
@model ContosoUniversity.Models.Department

@{
ViewData["Title"] = "Edit";
}

<h2>Edit</h2>

<h4>Department</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="Edit">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="DepartmentID" />
<input type="hidden" asp-for="RowVersion" />
<div class="form-group">
<label asp-for="Name" class="control-label"></label>
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Budget" class="control-label"></label>
<input asp-for="Budget" class="form-control" />
<span asp-validation-for="Budget" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="StartDate" class="control-label"></label>
<input asp-for="StartDate" class="form-control" />
<span asp-validation-for="StartDate" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="InstructorID" class="control-label"></label>
<select asp-for="InstructorID" class="form-control" asp-items="ViewBag.InstructorID">
<option value="">-- Select Administrator --</option>
</select>
<span asp-validation-for="InstructorID" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</form>
</div>
</div>

<div>
<a asp-action="Index">Back to List</a>
</div>

@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

Comprobar los conflictos de simultaneidad en la página Edit


Ejecute la aplicación y vaya a la página de índice de Departments. Haga clic con el botón derecho en el
hipervínculo Edit del departamento de inglés, seleccione Abrir en nueva pestaña y, después, haga clic en el
hipervínculo Edit del departamento de inglés. Las dos pestañas del explorador ahora muestran la misma
información.
Cambie un campo en la primera pestaña del explorador y haga clic en Save.
El explorador muestra la página de índice con el valor modificado.
Cambie un campo en la segunda pestaña del explorador.
Haga clic en Guardar. Verá un mensaje de error:
Vuelva a hacer clic en Save. Se guarda el valor especificado en la segunda pestaña del explorador. Verá los
valores guardados cuando aparezca la página de índice.

Actualizar la página Delete


Para la página Delete, Entity Framework detecta los conflictos de simultaneidad causados por una persona que
edita el departamento de forma similar. Cuando el método Delete de HttpGet muestra la vista de confirmación,
la vista incluye el valor RowVersion original en un campo oculto. Dicho valor está entonces disponible para el
método Delete de HttpPost al que se llama cuando el usuario confirma la eliminación. Cuando Entity
Framework crea el comando DELETE de SQL, incluye una cláusula WHERE con el valor RowVersion original. Si
el comando tiene como resultado cero filas afectadas (es decir, la fila se cambió después de que se muestre la
página de confirmación de eliminación), se produce una excepción de simultaneidad y el método Delete de
HttpGet se llama con una marca de error establecida en true para volver a mostrar la página de confirmación con
un mensaje de error. También es posible que se vieran afectadas cero filas porque otro usuario eliminó la fila, por
lo que en ese caso no se muestra ningún mensaje de error.
Actualizar los métodos Delete en el controlador de Departments
En DepartmentsController.cs, reemplace el método Delete de HttpGet por el código siguiente:
public async Task<IActionResult> Delete(int? id, bool? concurrencyError)
{
if (id == null)
{
return NotFound();
}

var department = await _context.Departments


.Include(d => d.Administrator)
.AsNoTracking()
.SingleOrDefaultAsync(m => m.DepartmentID == id);
if (department == null)
{
if (concurrencyError.GetValueOrDefault())
{
return RedirectToAction(nameof(Index));
}
return NotFound();
}

if (concurrencyError.GetValueOrDefault())
{
ViewData["ConcurrencyErrorMessage"] = "The record you attempted to delete "
+ "was modified by another user after you got the original values. "
+ "The delete operation was canceled and the current values in the "
+ "database have been displayed. If you still want to delete this "
+ "record, click the Delete button again. Otherwise "
+ "click the Back to List hyperlink.";
}

return View(department);
}

El método acepta un parámetro opcional que indica si la página volverá a aparecer después de un error de
simultaneidad. Si esta marca es true y el departamento especificado ya no existe, significa que otro usuario lo
eliminó. En ese caso, el código redirige a la página de índice. Si esta marca es true y el departamento existe,
significa que otro usuario lo modificó. En ese caso, el código envía un mensaje de error a la vista mediante
ViewData .

Reemplace el código en el método Delete de HttpPost (denominado DeleteConfirmed ) con el código siguiente:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Delete(Department department)
{
try
{
if (await _context.Departments.AnyAsync(m => m.DepartmentID == department.DepartmentID))
{
_context.Departments.Remove(department);
await _context.SaveChangesAsync();
}
return RedirectToAction(nameof(Index));
}
catch (DbUpdateConcurrencyException /* ex */)
{
//Log the error (uncomment ex variable name and write a log.)
return RedirectToAction(nameof(Delete), new { concurrencyError = true, id = department.DepartmentID
});
}
}

En el código al que se aplicó la técnica scaffolding que acaba de reemplazar, este método solo acepta un
identificador de registro:

public async Task<IActionResult> DeleteConfirmed(int id)

Ha cambiado este parámetro en una instancia de la entidad Department creada por el enlazador de modelos.
Esto proporciona a EF acceso al valor de la propiedad RowVersion, además de la clave de registro.

public async Task<IActionResult> Delete(Department department)

También ha cambiado el nombre del método de acción de DeleteConfirmed a Delete . El código al que se aplicó
la técnica scaffolding usa el nombre DeleteConfirmed para proporcionar al método HttpPost una firma única. (El
CLR requiere métodos sobrecargados para tener parámetros de método diferentes). Ahora que las firmas son
únicas, puede ceñirse a la convención MVC y usar el mismo nombre para los métodos de eliminación de
HttpPost y HttpGet.
Si ya se ha eliminado el departamento, el método AnyAsync devuelve false y la aplicación simplemente vuelve al
método de índice.
Si se detecta un error de simultaneidad, el código vuelve a mostrar la página de confirmación de Delete y
proporciona una marca que indica que se debería mostrar un mensaje de error de simultaneidad.
Actualizar la vista Delete
En Views/Departments/Delete.cshtml, reemplace el código al que se aplicó la técnica scaffolding con el siguiente
código, que agrega un campo de mensaje de error y campos ocultos para las propiedades DepartmentID y
RowVersion. Los cambios aparecen resaltados.
@model ContosoUniversity.Models.Department

@{
ViewData["Title"] = "Delete";
}

<h2>Delete</h2>

<p class="text-danger">@ViewData["ConcurrencyErrorMessage"]</p>

<h3>Are you sure you want to delete this?</h3>


<div>
<h4>Department</h4>
<hr />
<dl class="dl-horizontal">
<dt>
@Html.DisplayNameFor(model => model.Name)
</dt>
<dd>
@Html.DisplayFor(model => model.Name)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Budget)
</dt>
<dd>
@Html.DisplayFor(model => model.Budget)
</dd>
<dt>
@Html.DisplayNameFor(model => model.StartDate)
</dt>
<dd>
@Html.DisplayFor(model => model.StartDate)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Administrator)
</dt>
<dd>
@Html.DisplayFor(model => model.Administrator.FullName)
</dd>
</dl>

<form asp-action="Delete">
<input type="hidden" asp-for="DepartmentID" />
<input type="hidden" asp-for="RowVersion" />
<div class="form-actions no-color">
<input type="submit" value="Delete" class="btn btn-default" /> |
<a asp-action="Index">Back to List</a>
</div>
</form>
</div>

Esto realiza los cambios siguientes:


Agrega un mensaje de error entre los encabezados h2 y h3 .
Reemplaza FirstMidName por FullName en el campo Administrator.
Quita el campo RowVersion.
Agrega un campo oculto para la propiedad RowVersion .

Ejecute la aplicación y vaya a la página de índice de Departments. Haga clic con el botón derecho en el
hipervínculo Delete del departamento de inglés, seleccione Abrir en nueva pestaña y, después, en la primera
pestaña, haga clic en el hipervínculo Edit del departamento de inglés.
En la primera ventana, cambie uno de los valores y haga clic en Save:

En la segunda pestaña, haga clic en Delete. Verá el mensaje de error de simultaneidad y se actualizarán los
valores de Department con lo que está actualmente en la base de datos.
Si vuelve a hacer clic en Delete, se le redirigirá a la página de índice, que muestra que se ha eliminado el
departamento.

Actualizar las vistas Details y Create


Si quiere, puede limpiar el código al que se ha aplicado la técnica scaffolding en las vistas Details y Create.
Reemplace el código de Views/Departments/Details.cshtml para eliminar la columna RowVersion y mostrar el
nombre completo del administrador.
@model ContosoUniversity.Models.Department

@{
ViewData["Title"] = "Details";
}

<h2>Details</h2>

<div>
<h4>Department</h4>
<hr />
<dl class="dl-horizontal">
<dt>
@Html.DisplayNameFor(model => model.Name)
</dt>
<dd>
@Html.DisplayFor(model => model.Name)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Budget)
</dt>
<dd>
@Html.DisplayFor(model => model.Budget)
</dd>
<dt>
@Html.DisplayNameFor(model => model.StartDate)
</dt>
<dd>
@Html.DisplayFor(model => model.StartDate)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Administrator)
</dt>
<dd>
@Html.DisplayFor(model => model.Administrator.FullName)
</dd>
</dl>
</div>
<div>
<a asp-action="Edit" asp-route-id="@Model.DepartmentID">Edit</a> |
<a asp-action="Index">Back to List</a>
</div>

Reemplace el código de Views/Departments/Create.cshtml para agregar una opción Select en la lista desplegable.
@model ContosoUniversity.Models.Department

@{
ViewData["Title"] = "Create";
}

<h2>Create</h2>

<h4>Department</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="Create">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Name" class="control-label"></label>
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Budget" class="control-label"></label>
<input asp-for="Budget" class="form-control" />
<span asp-validation-for="Budget" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="StartDate" class="control-label"></label>
<input asp-for="StartDate" class="form-control" />
<span asp-validation-for="StartDate" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="InstructorID" class="control-label"></label>
<select asp-for="InstructorID" class="form-control" asp-items="ViewBag.InstructorID">
<option value="">-- Select Administrator --</option>
</select>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</form>
</div>
</div>

<div>
<a asp-action="Index">Back to List</a>
</div>

@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

Resumen
Con esto finaliza la introducción para el control de los conflictos de simultaneidad. Para obtener más información
sobre cómo administrar la simultaneidad en EF Core, vea Concurrency conflicts (Conflictos de simultaneidad). El
siguiente tutorial muestra cómo implementar la herencia de tabla por jerarquía para las entidades Instructor y
Student.

A N T E R IO R S IG U IE N T E
ASP.NET Core MVC con EF Core: herencia (9 de 10)
25/06/2018 • 14 minutes to read • Edit Online

Por Tom Dykstra y Rick Anderson


En la aplicación web de ejemplo Contoso University se muestra cómo crear aplicaciones web de ASP.NET Core
MVC con Entity Framework Core y Visual Studio. Para obtener información sobre la serie de tutoriales, consulte
el primer tutorial de la serie.
En el tutorial anterior, se trataron las excepciones de simultaneidad. En este tutorial se muestra cómo
implementar la herencia en el modelo de datos.
En la programación orientada a objetos, puede usar la herencia para facilitar la reutilización del código. En este
tutorial, cambiará las clases Instructor y Student para que deriven de una clase base Person que contenga
propiedades como LastName , que son comunes tanto para los instructores como para los alumnos. No tendrá
que agregar ni cambiar ninguna página web, sino que cambiará parte del código y esos cambios se reflejarán
automáticamente en la base de datos.

Opciones para asignar herencia a las tablas de bases de datos


Las clases Instructor y Student del modelo de datos School tienen varias propiedades idénticas:

Imagine que quiere eliminar el código redundante de las propiedades que comparten las entidades Instructor
y Student . O que quiere escribir un servicio que pueda dar formato a los nombres sin preocuparse de si el
nombre es de un instructor o de un alumno. Puede crear una clase base Person que solo contenga las
propiedades compartidas y después hacer que las clases Instructor y Student hereden de esa clase base,
como se muestra en la siguiente ilustración:
Esta estructura de herencia se puede representar de varias formas en la base de datos. Puede tener una sola
tabla Person que incluya información sobre los alumnos y los instructores. Algunas de las columnas podrían
aplicarse solo a los instructores (HireDate), otras solo a los alumnos (EnrollmentDate) y otras a ambos
(LastName, FirstName). Lo más común sería que tuviera una columna discriminadora para indicar qué tipo
representa cada fila. Por ejemplo, en la columna discriminadora podría aparecer "Instructor" para los
instructores y "Student" para los alumnos.

Este patrón, que consiste en generar una estructura de herencia de la entidad a partir de una tabla de base de
datos única, se denomina herencia de tabla por jerarquía (TPH).
Una alternativa consiste en hacer que la base de datos se parezca más a la estructura de herencia. Por ejemplo,
podría tener solo los campos de nombre en la tabla Person y tener tablas Instructor y Student independientes
con los campos de fecha.

Este patrón, que consiste en crear una tabla de base de datos para cada clase de entidad, se denomina herencia
de tabla por tipo (TPT).
Y todavía otra opción pasa por asignar todos los tipos no abstractos a tablas individuales. Todas las propiedades
de una clase, incluidas las propiedades heredadas, se asignan a columnas de la tabla correspondiente. Este
patrón se denomina herencia de tabla por clase concreta (TPC ). Si implementara la herencia de TPC en las
clases Person, Student e Instructor tal como se ha explicado anteriormente, las tablas Student e Instructor se
verían igual antes que después de implementar la herencia.
Los patrones de herencia de TPC y TPH acostumbran a tener un mejor rendimiento que los patrones de
herencia de TPT, porque estos pueden provocar consultas de combinaciones complejas.
Este tutorial muestra cómo implementar la herencia de TPH. TPH es el único patrón de herencia que Entity
Framework Core admite. Lo que tiene que hacer es crear una clase Person , cambiar las clases Instructor y
Student que deriven de Person , agregar la nueva clase a DbContext y crear una migración.

TIP
Considere la posibilidad de guardar una copia del proyecto antes de realizar los siguientes cambios. De este modo, si tiene
problemas y necesita volver a empezar, le resultará más fácil comenzar desde el proyecto guardado que tener que revertir
los pasos realizados en este tutorial o volver al principio de toda la serie.

Creación de la clase Person


En la carpeta Models, cree Person.cs y reemplace el código de plantilla por el código siguiente:

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
public abstract class Person
{
public int ID { get; set; }

[Required]
[StringLength(50)]
[Display(Name = "Last Name")]
public string LastName { get; set; }
[Required]
[StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
[Column("FirstName")]
[Display(Name = "First Name")]
public string FirstMidName { get; set; }

[Display(Name = "Full Name")]


public string FullName
{
get
{
return LastName + ", " + FirstMidName;
}
}
}
}

Asegúrese de que las clases Student e Instructor heredan de Person


En Instructor.cs, derive la clase Instructor de la clase Person y quite los campos de clave y nombre. El código
tendrá un aspecto similar al ejemplo siguiente:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
public class Instructor : Person
{
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
[Display(Name = "Hire Date")]
public DateTime HireDate { get; set; }

public ICollection<CourseAssignment> CourseAssignments { get; set; }


public OfficeAssignment OfficeAssignment { get; set; }
}
}

Realice los mismos cambios en Student.cs.

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
public class Student : Person
{
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
[Display(Name = "Enrollment Date")]
public DateTime EnrollmentDate { get; set; }

public ICollection<Enrollment> Enrollments { get; set; }


}
}

Agregar el tipo de entidad Person al modelo de datos


Agregue el tipo de entidad Person a SchoolContext.cs. Se resaltan las líneas nuevas.
using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity.Data
{
public class SchoolContext : DbContext
{
public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
{
}

public DbSet<Course> Courses { get; set; }


public DbSet<Enrollment> Enrollments { get; set; }
public DbSet<Student> Students { get; set; }
public DbSet<Department> Departments { get; set; }
public DbSet<Instructor> Instructors { get; set; }
public DbSet<OfficeAssignment> OfficeAssignments { get; set; }
public DbSet<CourseAssignment> CourseAssignments { get; set; }
public DbSet<Person> People { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)


{
modelBuilder.Entity<Course>().ToTable("Course");
modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
modelBuilder.Entity<Student>().ToTable("Student");
modelBuilder.Entity<Department>().ToTable("Department");
modelBuilder.Entity<Instructor>().ToTable("Instructor");
modelBuilder.Entity<OfficeAssignment>().ToTable("OfficeAssignment");
modelBuilder.Entity<CourseAssignment>().ToTable("CourseAssignment");
modelBuilder.Entity<Person>().ToTable("Person");

modelBuilder.Entity<CourseAssignment>()
.HasKey(c => new { c.CourseID, c.InstructorID });
}
}
}

Esto es todo lo que Entity Framework necesita para configurar la herencia de tabla por jerarquía. Como verá,
cuando la base de datos esté actualizada, tendrá una tabla Person en lugar de las tablas Student e Instructor.

Creación y personalización del código de migración


Guarde los cambios y compile el proyecto. A continuación, abra la ventana de comandos en la carpeta de
proyecto y escriba el siguiente comando:

dotnet ef migrations add Inheritance

No ejecute el comando database update todavía. Este comando provocará la pérdida de datos porque colocará
la tabla Instructor y cambiará el nombre de la tabla Student por Person. Deberá proporcionar código
personalizado para conservar los datos existentes.
Abra Migrations/<marca_de_tiempo>_Inheritance.cs y reemplace el método Up por el código siguiente:
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Enrollment_Student_StudentID",
table: "Enrollment");

migrationBuilder.DropIndex(name: "IX_Enrollment_StudentID", table: "Enrollment");

migrationBuilder.RenameTable(name: "Instructor", newName: "Person");


migrationBuilder.AddColumn<DateTime>(name: "EnrollmentDate", table: "Person", nullable: true);
migrationBuilder.AddColumn<string>(name: "Discriminator", table: "Person", nullable: false, maxLength:
128, defaultValue: "Instructor");
migrationBuilder.AlterColumn<DateTime>(name: "HireDate", table: "Person", nullable: true);
migrationBuilder.AddColumn<int>(name: "OldId", table: "Person", nullable: true);

// Copy existing Student data into new Person table.


migrationBuilder.Sql("INSERT INTO dbo.Person (LastName, FirstName, HireDate, EnrollmentDate,
Discriminator, OldId) SELECT LastName, FirstName, null AS HireDate, EnrollmentDate, 'Student' AS
Discriminator, ID AS OldId FROM dbo.Student");
// Fix up existing relationships to match new PK's.
migrationBuilder.Sql("UPDATE dbo.Enrollment SET StudentId = (SELECT ID FROM dbo.Person WHERE OldId =
Enrollment.StudentId AND Discriminator = 'Student')");

// Remove temporary key


migrationBuilder.DropColumn(name: "OldID", table: "Person");

migrationBuilder.DropTable(
name: "Student");

migrationBuilder.CreateIndex(
name: "IX_Enrollment_StudentID",
table: "Enrollment",
column: "StudentID");

migrationBuilder.AddForeignKey(
name: "FK_Enrollment_Person_StudentID",
table: "Enrollment",
column: "StudentID",
principalTable: "Person",
principalColumn: "ID",
onDelete: ReferentialAction.Cascade);
}

Este código se encarga de las siguientes tareas de actualización de la base de datos:


Quita las restricciones de la clave externa y los índices que apuntan a la tabla Student.
Cambia el nombre de la tabla Instructor por Person y realiza los cambios necesarios para que pueda
almacenar datos de los alumnos:
Agrega EnrollmentDate que acepta valores NULL para alumnos.
Agrega la columna discriminadora para indicar si una fila es para un alumno o para un instructor.
Hace que HireDate admita un valor NULL, puesto que las filas de alumnos no dispondrán de fechas de
contratación.
Agrega un campo temporal que se usará para actualizar las claves externas que apuntan a los alumnos.
Cuando copie alumnos en la tabla Person, obtendrán nuevos valores de clave principal.
Copia datos de la tabla Student a la tabla Person. Esto hace que los alumnos se asignen a nuevos valores
de clave principal.
Corrige los valores de clave externa correspondientes a los alumnos.
Vuelve a crear las restricciones y los índices de la clave externa, pero ahora los dirige a la tabla Person.
(Si hubiera usado el GUID en lugar de un número entero como tipo de clave principal, los valores de la clave
principal de alumno no tendrían que cambiar y algunos de estos pasos se podrían haber omitido).
Ejecute el comando database update :

dotnet ef database update

(En un sistema de producción haría los cambios correspondientes en el método Down por si alguna vez tuviera
que usarlo para volver a la versión anterior de la base de datos. Para este tutorial, no se usará el método Down ).

NOTE
Al hacer cambios en el esquema, se pueden generar otros errores en una base de datos que contenga los datos
existentes. Si se producen errores de migración que no se pueden resolver, puede cambiar el nombre de la base de datos
en la cadena de conexión o eliminar la base de datos. Con una nueva base de datos, no habrá ningún dato para migrar y
es más probable que el comando de actualización de base de datos se complete sin errores. Para eliminar la base de datos,
use SSOX o ejecute el comando database drop de la CLI.

Prueba con herencia implementada


Ejecute la aplicación y haga la prueba en distintas páginas. Todo funciona igual que antes.
En Explorador de objetos de SQL Server, expanda Conexiones de datos/SchoolContext y después
Tables, y verá que las tablas Student e Instructor se han reemplazado por una tabla Person. Abra el diseñador
de la tabla Person y verá que contiene todas las columnas que solía haber en las tablas Student e Instructor.

Haga clic con el botón derecho en la tabla Person y después haga clic en Mostrar datos de tabla para ver la
columna discriminadora.
Resumen
Ha implementado la herencia de tabla por jerarquía en las clases Person , Student y Instructor . Para obtener
más información sobre la herencia en Entity Framework Core, consulte Herencia. En el siguiente tutorial,
aprenderá a controlar una serie de escenarios de Entity Framework relativamente avanzados.

A N T E R IO R S IG U IE N T E
ASP.NET Core MVC con EF Core: Avanzado (10 de
10)
25/06/2018 • 24 minutes to read • Edit Online

Por Tom Dykstra y Rick Anderson


En la aplicación web de ejemplo Contoso University se muestra cómo crear aplicaciones web de ASP.NET Core
MVC con Entity Framework Core y Visual Studio. Para obtener información sobre la serie de tutoriales, consulte
el primer tutorial de la serie.
En el tutorial anterior, se implementó la herencia de tabla por jerarquía. En este tutorial se presentan varios
temas que es importante tener en cuenta cuando va más allá de los conceptos básicos del desarrollo de
aplicaciones web ASP.NET Core que usan Entity Framework Core.

Consultas SQL sin formato


Una de las ventajas del uso de Entity Framework es que evita enlazar el código demasiado estrechamente a un
método concreto de almacenamiento de datos. Lo consigue mediante la generación de consultas SQL y
comandos, lo que también le evita tener que escribirlos usted mismo. Pero hay situaciones excepcionales en las
que necesita ejecutar consultas específicas de SQL que ha creado manualmente. En estos casos, la API de Entity
Framework Code First incluye métodos que le permiten pasar comandos SQL directamente a la base de datos.
En EF Core 1.0 dispone de las siguientes opciones:
Use el método DbSet.FromSql para las consultas que devuelven tipos de entidad. Los objetos devueltos
deben ser del tipo esperado por el objeto DbSet y se les realiza automáticamente un seguimiento
mediante el contexto de base de datos a menos que se desactive el seguimiento.
Use Database.ExecuteSqlCommand para comandos sin consulta.
Si tiene que ejecutar una consulta que devuelve tipos que no son entidades, puede usar ADO.NET con la
conexión de base de datos proporcionada por EF. No se realiza un seguimiento de los datos devueltos por el
contexto de la base de datos, incluso si usa este método para recuperar tipos de entidad.
Como siempre es true cuando ejecuta comandos SQL en una aplicación web, debe tomar precauciones para
proteger su sitio contra los ataques por inyección de código SQL. Una manera de hacerlo es mediante consultas
parametrizadas para asegurarse de que las cadenas enviadas por una página web no se pueden interpretar
como comandos SQL. En este tutorial usará las consultas con parámetros al integrar la entrada de usuario en
una consulta.

Llamar a una consulta que devuelve entidades


La clase DbSet<TEntity> proporciona un método que puede usar para ejecutar una consulta que devuelve una
entidad de tipo TEntity . Para ver cómo funciona, cambiará el código en el método Details del controlador de
departamento.
En DepartmentsController.cs, en el método Details , reemplace el código que recupera un departamento con
una llamada al método FromSql , como se muestra en el código resaltado siguiente:
public async Task<IActionResult> Details(int? id)
{
if (id == null)
{
return NotFound();
}

string query = "SELECT * FROM Department WHERE DepartmentID = {0}";


var department = await _context.Departments
.FromSql(query, id)
.Include(d => d.Administrator)
.AsNoTracking()
.SingleOrDefaultAsync();

if (department == null)
{
return NotFound();
}

return View(department);
}

Para comprobar que el nuevo código funciona correctamente, seleccione la pestaña Departments y, después,
Details para uno de los departamentos.

Llamar a una consulta que devuelve otros tipos


Anteriormente creó una cuadrícula de estadísticas de alumno de la página About que mostraba el número de
alumnos para cada fecha de inscripción. Obtuvo los datos del conjunto de entidades Students (
_context.Students ) y usó LINQ para proyectar los resultados en una lista de objetos de modelo de vista
EnrollmentDateGroup . Suponga que quiere escribir la instrucción SQL propia en lugar de usar LINQ. Para ello,
necesita ejecutar una consulta SQL que devuelve un valor distinto de objetos entidad. En EF Core 1.0, una
manera de hacerlo es escribir código de ADO.NET y obtener la conexión de base de datos de EF.
En HomeController.cs, reemplace el método About por el código siguiente:
public async Task<ActionResult> About()
{
List<EnrollmentDateGroup> groups = new List<EnrollmentDateGroup>();
var conn = _context.Database.GetDbConnection();
try
{
await conn.OpenAsync();
using (var command = conn.CreateCommand())
{
string query = "SELECT EnrollmentDate, COUNT(*) AS StudentCount "
+ "FROM Person "
+ "WHERE Discriminator = 'Student' "
+ "GROUP BY EnrollmentDate";
command.CommandText = query;
DbDataReader reader = await command.ExecuteReaderAsync();

if (reader.HasRows)
{
while (await reader.ReadAsync())
{
var row = new EnrollmentDateGroup { EnrollmentDate = reader.GetDateTime(0), StudentCount
= reader.GetInt32(1) };
groups.Add(row);
}
}
reader.Dispose();
}
}
finally
{
conn.Close();
}
return View(groups);
}

Agregue una instrucción using:

using System.Data.Common;

Ejecute la aplicación y vaya a la página About. Muestra los mismos datos que anteriormente.

Llamar a una consulta update


Imagine que los administradores de Contoso University quieren realizar cambios globales en la base de datos,
como cambiar el número de créditos para cada curso. Si la universidad tiene un gran número de cursos, sería
poco eficaz recuperarlos todos como entidades y cambiarlos de forma individual. En esta sección implementará
una página web que permite al usuario especificar un factor por el cual se va a cambiar el número de créditos
para todos los cursos y podrá realizar el cambio mediante la ejecución de una instrucción UPDATE de SQL. La
página web tendrá el mismo aspecto que la ilustración siguiente:

En CoursesContoller.cs, agregue los métodos UpdateCourseCredits para HttpGet y HttpPost:

public IActionResult UpdateCourseCredits()


{
return View();
}

[HttpPost]
public async Task<IActionResult> UpdateCourseCredits(int? multiplier)
{
if (multiplier != null)
{
ViewData["RowsAffected"] =
await _context.Database.ExecuteSqlCommandAsync(
"UPDATE Course SET Credits = Credits * {0}",
parameters: multiplier);
}
return View();
}

Cuando el controlador procesa una solicitud HttpGet, no se devuelve nada en ViewData["RowsAffected"] y la


vista muestra un cuadro de texto vacío y un botón de envío, tal como se muestra en la ilustración anterior.
Cuando se hace clic en el botón Update, se llama al método HttpPost y el multiplicador tiene el valor
especificado en el cuadro de texto. A continuación, el código ejecuta la instrucción SQL que actualiza los cursos y
devuelve el número de filas afectadas a la vista en ViewData . Cuando la vista obtiene un valor RowsAffected ,
muestra el número de filas actualizadas.
En el Explorador de soluciones, haga clic con el botón derecho en la carpeta Views/Courses y luego haga clic
en Agregar > Nuevo elemento.
En el cuadro de diálogo Agregar nuevo elemento, haga clic en ASP.NET en Instalado en el panel izquierdo,
haga clic en Página de la vista de MVC y nombre la nueva vista UpdateCourseCredits.cshtml.
En Views/Courses/UpdateCourseCredits.cshtml, reemplace el código de plantilla con el código siguiente:
@{
ViewBag.Title = "UpdateCourseCredits";
}

<h2>Update Course Credits</h2>

@if (ViewData["RowsAffected"] == null)


{
<form asp-action="UpdateCourseCredits">
<div class="form-actions no-color">
<p>
Enter a number to multiply every course's credits by: @Html.TextBox("multiplier")
</p>
<p>
<input type="submit" value="Update" class="btn btn-default" />
</p>
</div>
</form>
}
@if (ViewData["RowsAffected"] != null)
{
<p>
Number of rows updated: @ViewData["RowsAffected"]
</p>
}
<div>
@Html.ActionLink("Back to List", "Index")
</div>

Ejecute el método UpdateCourseCredits seleccionando la pestaña Courses, después, agregue


"/UpdateCourseCredits" al final de la dirección URL en la barra de direcciones del explorador (por ejemplo:
http://localhost:5813/Courses/UpdateCourseCredits ). Escriba un número en el cuadro de texto:

Haga clic en Actualizar. Verá el número de filas afectadas:


Haga clic en Volver a la lista para ver la lista de cursos con el número de créditos revisado.
Tenga en cuenta que el código de producción garantiza que las actualizaciones siempre dan como resultado
datos válidos. El código simplificado que se muestra a continuación podría multiplicar el número de créditos lo
suficiente para que el resultado sea un número superior a 5. (La propiedad Credits tiene un atributo
[Range(0, 5)] ). La consulta update podría funcionar, pero los datos no válidos podrían provocar resultados
inesperados en otras partes del sistema que asumen que el número de créditos es igual o inferior a 5.
Para obtener más información sobre las consultas SQL básicas, vea Consultas SQL básicas.

Examinar el código SQL enviado a la base de datos


A veces resulta útil poder ver las consultas SQL reales que se envían a la base de datos. EF Core usa
automáticamente la funcionalidad de registro integrada para ASP.NET Core para escribir los registros que
contienen el código SQL para las consultas y actualizaciones. En esta sección podrá ver algunos ejemplos de
registros de SQL.
Abra StudentsController.cs y, en el método Details , establezca un punto de interrupción en la instrucción
if (student == null) .

Ejecute la aplicación en modo de depuración y vaya a la página Details de un alumno.


Vaya a la ventana Salida que muestra la salida de depuración y verá la consulta:

Microsoft.EntityFrameworkCore.Database.Command:Information: Executed DbCommand (56ms) [Parameters=


[@__id_0='?'], CommandType='Text', CommandTimeout='30']
SELECT TOP(2) [s].[ID], [s].[Discriminator], [s].[FirstName], [s].[LastName], [s].[EnrollmentDate]
FROM [Person] AS [s]
WHERE ([s].[Discriminator] = N'Student') AND ([s].[ID] = @__id_0)
ORDER BY [s].[ID]
Microsoft.EntityFrameworkCore.Database.Command:Information: Executed DbCommand (122ms) [Parameters=
[@__id_0='?'], CommandType='Text', CommandTimeout='30']
SELECT [s.Enrollments].[EnrollmentID], [s.Enrollments].[CourseID], [s.Enrollments].[Grade], [s.Enrollments].
[StudentID], [e.Course].[CourseID], [e.Course].[Credits], [e.Course].[DepartmentID], [e.Course].[Title]
FROM [Enrollment] AS [s.Enrollments]
INNER JOIN [Course] AS [e.Course] ON [s.Enrollments].[CourseID] = [e.Course].[CourseID]
INNER JOIN (
SELECT TOP(1) [s0].[ID]
FROM [Person] AS [s0]
WHERE ([s0].[Discriminator] = N'Student') AND ([s0].[ID] = @__id_0)
ORDER BY [s0].[ID]
) AS [t] ON [s.Enrollments].[StudentID] = [t].[ID]
ORDER BY [t].[ID]

Aquí verá algo que podría sorprenderle: la instrucción SQL selecciona hasta 2 filas ( TOP(2) ) de la tabla Person.
El método SingleOrDefaultAsync no se resuelve en 1 fila en el servidor. El motivo es el siguiente:
Si la consulta devolvería varias filas, el método devuelve NULL.
Para determinar si la consulta devolvería varias filas, EF tiene que comprobar si devuelve al menos 2.
Tenga en cuenta que no tiene que usar el modo de depuración y parar en un punto de interrupción para obtener
los resultados del registro en la ventana Salida. Es una manera cómoda de detener el registro en el punto que
quiere ver en la salida. Si no lo hace, el registro continúa y tiene que desplazarse hacia atrás para encontrar los
elementos que le interesan.

Patrones de repositorio y de unidad de trabajo


Muchos desarrolladores escriben código para implementar el repositorio y una unidad de patrones de trabajo
como un contenedor en torno al código que funciona con Entity Framework. Estos patrones están previstos para
crear una capa de abstracción entre la capa de acceso de datos y la capa de lógica de negocios de una aplicación.
Implementar estos patrones puede ayudar a aislar la aplicación de cambios en el almacén de datos y puede
facilitar la realización de pruebas unitarias automatizadas o el desarrollo controlado por pruebas (TDD ). Pero
escribir código adicional para implementar estos patrones no siempre es la mejor opción para las aplicaciones
que usan EF, por varias razones:
La propia clase de contexto de EF aísla el código del código específico del almacén de datos.
La clase de contexto de EF puede actuar como una clase de unidad de trabajo para las actualizaciones de
base de datos que hace con EF.
EF incluye características para implementar TDD sin escribir código de repositorio.
Para obtener información sobre cómo implementar los patrones de repositorio y de unidad de trabajo, consulte
la versión de Entity Framework 5 de esta serie de tutoriales.
Entity Framework Core implementa un proveedor de base de datos en memoria que puede usarse para realizar
pruebas. Para más información, vea Pruebas con InMemory.

Detección de cambios automática


Entity Framework determina cómo ha cambiado una entidad (y, por tanto, las actualizaciones que hay que enviar
a la base de datos) comparando los valores actuales de una entidad con los valores originales. Cuando se
consulta o se adjunta la entidad, se almacenan los valores originales. Algunos de los métodos que provocan la
detección de cambios automática son los siguientes:
DbContext.SaveChanges
DbContext.Entry
ChangeTracker.Entries
Si está realizando el seguimiento de un gran número de entidades y llama a uno de estos métodos muchas
veces en un bucle, podría obtener mejoras de rendimiento significativas si desactiva temporalmente la detección
de cambios automática mediante la propiedad ChangeTracker.AutoDetectChangesEnabled . Por ejemplo:

_context.ChangeTracker.AutoDetectChangesEnabled = false;

Código fuente y planes de desarrollo de Entity Framework Core


El origen de Entity Framework Core está en https://github.com/aspnet/EntityFrameworkCore. El repositorio de
EF Core contiene las compilaciones nocturnas, el seguimiento de problemas, las especificaciones de
características, las notas de las reuniones de diseño y el plan de desarrollo futuro. Puede archivar o buscar
errores y contribuir.
Aunque el código fuente es abierto, Entity Framework Core es totalmente compatible como producto de
Microsoft. El equipo de Microsoft Entity Framework mantiene el control sobre qué contribuciones se aceptan y
comprueba todos los cambios de código para garantizar la calidad de cada versión.

Ingeniería inversa desde la base de datos existente


Para usar técnicas de ingeniería inversa a un modelo de datos, incluidas las clases de entidad de una base de
datos existente, use el comando scaffold dbcontext. Consulte el tutorial de introducción.

Usar LINQ dinámico para simplificar la ordenación del código de


selección
El tercer tutorial de esta serie muestra cómo escribir código LINQ mediante el codificado de forma rígida de los
nombres de columna en una instrucción switch . Con dos columnas entre las que elegir, esto funciona bien, pero
si tiene muchas columnas, el código podría considerarse detallado. Para solucionar este problema, puede usar el
método EF.Property para especificar el nombre de la propiedad como una cadena. Para probar este enfoque,
reemplace el método Index en StudentsController con el código siguiente.
public async Task<IActionResult> Index(
string sortOrder,
string currentFilter,
string searchString,
int? page)
{
ViewData["CurrentSort"] = sortOrder;
ViewData["NameSortParm"] =
String.IsNullOrEmpty(sortOrder) ? "LastName_desc" : "";
ViewData["DateSortParm"] =
sortOrder == "EnrollmentDate" ? "EnrollmentDate_desc" : "EnrollmentDate";

if (searchString != null)
{
page = 1;
}
else
{
searchString = currentFilter;
}

ViewData["CurrentFilter"] = searchString;

var students = from s in _context.Students


select s;

if (!String.IsNullOrEmpty(searchString))
{
students = students.Where(s => s.LastName.Contains(searchString)
|| s.FirstMidName.Contains(searchString));
}

if (string.IsNullOrEmpty(sortOrder))
{
sortOrder = "LastName";
}

bool descending = false;


if (sortOrder.EndsWith("_desc"))
{
sortOrder = sortOrder.Substring(0, sortOrder.Length - 5);
descending = true;
}

if (descending)
{
students = students.OrderByDescending(e => EF.Property<object>(e, sortOrder));
}
else
{
students = students.OrderBy(e => EF.Property<object>(e, sortOrder));
}

int pageSize = 3;
return View(await PaginatedList<Student>.CreateAsync(students.AsNoTracking(),
page ?? 1, pageSize));
}

Pasos siguientes
Con esto finaliza esta serie de tutoriales sobre cómo usar Entity Framework Core en una aplicación ASP.NET
MVC.
Para obtener más información sobre EF Core, consulte la documentación de Entity Framework Core. También
está disponible un libro: Entity Framework Core in Action (Entity Framework Core en acción, en inglés).
Para obtener información sobre cómo implementar una aplicación web, consulte Hospedaje e implementación.
Para obtener información sobre otros temas relacionados con ASP.NET Core MVC, como la autenticación y
autorización, consulte la documentación de ASP.NET Core.

Agradecimientos
Tom Dykstra y Rick Anderson (Twitter @RickAndMSFT) escribieron este tutorial. Rowan Miller, Diego Vega y
otros miembros del equipo de Entity Framework participaron con revisiones de código y ayudaron a depurar
problemas que surgieron mientras se estaba escribiendo el código para los tutoriales.

Errores comunes
ContosoUniversity.dll usado por otro proceso
Mensaje de error:

No se puede abrir '...bin\Debug\netcoreapp1.0\ContosoUniversity.dll' para escribir: El proceso no puede


obtener acceso al archivo '...\bin\Debug\netcoreapp1.0\ContosoUniversity.dll', otro proceso lo está usando.

Solución:
Detenga el sitio en IIS Express. Vaya a la bandeja del sistema de Windows, busque IIS Express y haga clic con el
botón derecho en su icono; seleccione el sitio de Contoso University y, después, haga clic en Detener sitio.
La migración aplicó la técnica scaffolding sin código en los métodos Up y Down
Causa posible:
Los comandos de la CLI de EF no cierran y guardan automáticamente los archivos de código. Si no ha guardado
los cambios cuando ejecuta el comando migrations add , EF no encontrará los cambios.
Solución:
Ejecute el comando migrations remove , guarde los cambios de código y vuelva a ejecutar el comando
migrations add .

Errores durante la ejecución de la actualización de la base de datos


Al hacer cambios en el esquema, se pueden generar otros errores en una base de datos que contenga los datos
existentes. Si se producen errores de migración que no se pueden resolver, puede cambiar el nombre de la base
de datos en la cadena de conexión o eliminar la base de datos. Con una base de datos nueva, no hay ningún dato
para migrar y es mucho más probable que el comando de actualización de base de datos se complete sin
errores.
El enfoque más sencillo consiste en cambiar el nombre de la base de datos en appsettings.json. La próxima vez
que ejecute database update , se creará una base de datos.
Para eliminar una base de datos en SSOX, haga clic con el botón derecho en la base de datos, haga clic en
Eliminar y, después, en el cuadro de diálogo Eliminar base de datos, seleccione Cerrar conexiones
existentes y haga clic en Aceptar.
Para eliminar una base de datos mediante el uso de la CLI, ejecute el comando de la CLI database drop :

dotnet ef database drop

Error al buscar la instancia de SQL Server


Mensaje de error:
Error relacionado con la red o específico de la instancia mientras se establecía una conexión con el servidor
SQL Server. No se encontró el servidor o éste no estaba accesible. Compruebe que el nombre de la instancia
es correcto y que SQL Server está configurado para admitir conexiones remotas. (proveedor: interfaces de
red de SQL, error: 26 - Error al buscar el servidor o instancia especificado)

Solución:
Compruebe la cadena de conexión. Si ha eliminado manualmente el archivo de base de datos, cambie el nombre
de la base de datos en la cadena de construcción para volver a empezar con una base de datos nueva.

A N T E R IO R
Tutoriales de ASP.NET Core
25/06/2018 • 2 minutes to read • Edit Online

Están disponibles las siguientes guías detalladas para desarrollar aplicaciones de ASP.NET Core:

Compilación de aplicaciones web


Las páginas de Razor son el método recomendado para crear una aplicación de interfaz de usuario web con
ASP.NET Core 2.0 y versiones posteriores.
Introducción a las páginas de Razor en ASP.NET Core
Creación de una aplicación web de páginas de Razor con ASP.NET Core
Páginas de Razor en macOS
Páginas de Razor con VSCode
Creación de una aplicación web de ASP.NET Core MVC
Aplicación web con Visual Studio para Mac
Aplicación web con Visual Studio Code en macOS o Linux

Compilación de API web


Creación de una API web con ASP.NET Core
API web con Visual Studio para Mac
API web con Visual Studio Code
Creación de una aplicación web de páginas de Razor
con ASP.NET Core en macOS con Visual Studio para
Mac
21/06/2018 • 2 minutes to read • Edit Online

Se está trabajando en este artículo.


En esta serie se explican los conceptos básicos de la creación de una aplicación web de páginas de Razor con
ASP.NET Core en macOS.
1. Introducción a las páginas de Razor en macOS
2. Adición de un modelo a una aplicación de páginas de Razor
3. Páginas de Razor con scaffolding
4. Trabajar con SQLite
5. Actualización de las páginas
6. Agregar búsqueda
Hasta que se complete la siguiente sección, siga la versión de Visual Studio para Windows.
1. Agregar un campo nuevo
2. Agregar validación
Introducción a las páginas de Razor en ASP.NET Core
en macOS con Visual Studio para Mac
25/06/2018 • 5 minutes to read • Edit Online

Por Rick Anderson


En este tutorial se enseñan los conceptos básicos de la compilación de una aplicación web de páginas de Razor de
ASP.NET Core. Se recomienda leer Introduction to Razor Pages (Introducción a las páginas de Razor) antes de
empezar este tutorial. Las páginas de Razor son el método recomendado para crear la interfaz de usuario de
aplicaciones web en ASP.NET Core.

Requisitos previos
Visual Studio for Mac

Creación de una aplicación web de Razor


Desde un terminal, ejecute estos comandos:

dotnet new webapp -o RazorPagesMovie


cd RazorPagesMovie
dotnet run

NOTE
In ASP.NET Core 2.1 or later, webapp is an alias of the razor argument. If the dotnet new webapp <OPTIONS> command
loads the dotnet new command help instead of creating a new Razor Pages app, install the .NET Core 2.1 SDK.

dotnet new razor -o RazorPagesMovie


cd RazorPagesMovie
dotnet run

Los comandos anteriores usan el CLI de .NET Core para crear y ejecutar un proyecto de páginas de Razor. Abra
http://localhost:5000 en un explorador para ver la aplicación.
La plantilla predeterminada crea los vínculos y las páginas RazorPagesMovie, Inicio, Sobre y Contacto. Según
el tamaño de la ventana del explorador, tendrá que hacer clic en el icono de navegación para mostrar los vínculos.

Pruebe los vínculos. Los vínculos RazorPagesMovie e Inicio dirigen a la página de índice. Los vínculos Acerca
de y Contacto dirigen a las páginas About y Contact respectivamente.

Archivos y carpetas del proyecto


En la tabla siguiente se enumeran los archivos y las carpetas del proyecto. En este tutorial, el archivo Startup.cs es
el más importante. No es necesario revisar todos los vínculos siguientes. Los vínculos se proporcionan como
referencia para cuando necesite más información sobre un archivo o una carpeta del proyecto.

ARCHIVO O CARPETA PROPÓSITO

wwwroot Contiene archivos estáticos. Vea Archivos estáticos.

Páginas Carpeta para páginas de Razor.

appsettings.json Configuración

Program.cs Aloja la aplicación de ASP.NET Core.

Startup.cs Configura los servicios y la canalización de solicitudes.


Consulte Inicio.

Carpeta Páginas
El archivo _Layout.cshtml contiene elementos HTML comunes (scripts y hojas de estilos) y establece el diseño de
la aplicación. Por ejemplo, al hacer clic en RazorPagesMovie, Inicio, Acerca de o Contacto, verá los mismos
elementos. Los elementos comunes incluyen el menú de navegación de la parte superior y el encabezado de la
parte inferior de la ventana. Consulte Diseño para obtener más información.
El archivo _ViewStart.cshtml establece la propiedad Layout de las páginas de Razor que para usar el archivo
_Layout.cshtml. Vea Layout (Diseño) para más información.
El archivo _ViewImports.cshtml contiene directivas de Razor que se importan en cada página de Razor. Consulte
Importing Shared Directives (Importar directivas compartidas) para obtener más información.
El archivo _ValidationScriptsPartial.cshtml proporciona una referencia de los scripts de validación de jQuery. Al
agregar las páginas Create y Edit más adelante en el tutorial, se usará el archivo
_ValidationScriptsPartial.cshtml.
Las páginas About , Contact y Index son páginas básicas que puede usar para iniciar una aplicación. La página
Error se usa para mostrar información de errores.

Abrir el proyecto
Presione CTRL+C para cerrar la aplicación.
En Visual Studio, seleccione Archivo > Abrir y elija el archivo RazorPagesMovie.csproj.
Iniciar la aplicación
En Visual Studio, seleccione Ejecutar > Iniciar sin depurar para iniciar la aplicación. Visual Studio iniciará
Kestrel y un explorador, y se desplazará a http://localhost:5000 .
En el tutorial siguiente, agregaremos un modelo al proyecto.

S IG U IE N T E : A D IC IÓ N D E U N
M ODELO
Agregar un modelo a una aplicación de páginas de
Razor de ASP.NET Core con Visual Studio para Mac
25/06/2018 • 9 minutes to read • Edit Online

Por Rick Anderson


En esta sección, agregará las clases para administrar películas en una base de datos. Estas clases se usan con
Entity Framework Core (EF Core) para trabajar con una base de datos. EF Core es un marco de trabajo de
asignación relacional de objetos (ORM ) que simplifica el código de acceso de datos que se debe escribir.
Las clases de modelo que se crean se conocen como clases POCO (del inglés "plain-old CLR objects", objetos
CLR antiguos sin formato) porque no tienen ninguna dependencia de EF Core. Definen las propiedades de los
datos que se almacenan en la base de datos.
En este tutorial, se escriben primero las clases del modelo y EF Core crea la base de datos. Hay un enfoque
alternativo, que no se trata aquí, que consiste en generar clases del modelo a partir de una base de datos existente.
Vea o descargue un ejemplo.

Agregar un modelo de datos


En el Explorador de soluciones, haga clic con el botón derecho en el proyecto RazorPagesMovie y
seleccione Agregar > Carpeta nueva. Asigne un nombre a la carpeta Modelos.
Haga clic con el botón derecho en la carpeta Modelos y seleccione Agregar > Archivo nuevo.
En el cuadro de diálogo Nuevo archivo:
Seleccione General en el panel izquierdo.
Seleccione Clase vacía en el panel central.
Asigne un nombre a la clase Película y seleccione Nuevo.
Agregue las propiedades siguientes a la clase Movie :

using System;

namespace RazorPagesMovie.Models
{
public class Movie
{
public int ID { get; set; }
public string Title { get; set; }
public DateTime ReleaseDate { get; set; }
public string Genre { get; set; }
public decimal Price { get; set; }
}
}

La base de datos requiere el campo ID para la clave principal.


Agregar una clase de contexto de base de datos
Agregue la clase MovieContext.cs siguiente a la carpeta Models:
using Microsoft.EntityFrameworkCore;

namespace RazorPagesMovie.Models
{
public class MovieContext : DbContext
{
public MovieContext(DbContextOptions<MovieContext> options)
: base(options)
{
}

public DbSet<Movie> Movie { get; set; }


}
}

El código anterior crea una propiedad DbSet para el conjunto de entidades. En la terminología de Entity
Framework, un conjunto de entidades suele corresponderse con una tabla de base de datos, mientras que una
entidad lo hace con una fila de la tabla.
Agregar una cadena de conexión de base de datos
Agregue una cadena de conexión al archivo appsettings.json.

{
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Warning"
}
},
"ConnectionStrings": {
"MovieContext": "Data Source=MvcMovie.db"
}
}

Registrar el contexto de base de datos


Registre el contexto de base de datos con el contenedor de inserción de dependencias en el archivo Startup.cs.

public void ConfigureServices(IServiceCollection services)


{
// requires
// using RazorPagesMovie.Models;
// using Microsoft.EntityFrameworkCore;

services.AddDbContext<MovieContext>(options =>
options.UseSqlite(Configuration.GetConnectionString("MovieContext")));
services.AddMvc();
}

Haga clic con el botón derecho en una línea ondulada roja, como MovieContext en la línea
services.AddDbContext<MovieContext>(options => . Seleccione Corrección rápida > con
RazorPagesMovie.Models;. Visual Studio agregará la instrucción de uso.
Compile el proyecto para comprobar que no contenga errores.
Paquetes de Entity Framework Core NuGet para migraciones
Las herramientas de EF para la interfaz de nivel de la línea de comandos se proporcionan en
Microsoft.EntityFrameworkCore.Tools.DotNet. Haga clic en el vínculo
Microsoft.EntityFrameworkCore.Tools.DotNet para obtener el número de versión que debe usar. Para instalar este
paquete, agréguelo a la colección DotNetCliToolReference del archivo .csproj. Nota: Tendrá que instalar este
paquete mediante la edición del archivo .csproj; no se puede usar el comando install-package ni la interfaz
gráfica de usuario del administrador de paquetes.
Para editar un archivo .csproj, haga lo siguiente:
Seleccione Archivo > Abrir y, después, seleccione el archivo .csproj.
Seleccione Opciones.
Cambie Abrir con a Editor de código fuente.

Agregue la referencia de herramientas de Microsoft.EntityFrameworkCore.Tools.DotNet al segundo <ItemGroup>:


<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.3" />
</ItemGroup>
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.2" />
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.1" />
</ItemGroup>
</Project>

Los números de versión que se muestran en el código que hay a continuación eran correctos en el momento de
escribir este artículo.

Agregar herramientas de scaffolding y realizar la migración inicial


Desde la línea de comandos, ejecute los comandos CLI de .NET Core:

dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design


dotnet restore
dotnet ef migrations add InitialCreate
dotnet ef database update

El comando add package instala las herramientas necesarias para ejecutar el motor de scaffolding.
El comando ef migrations add InitialCreate genera el código para crear el esquema de base de datos inicial. El
esquema se basa en el modelo especificado en DbContext (en el archivo Models/MovieContext.cs). El argumento
InitialCreate se usa para asignar un nombre a las migraciones. Puede usar cualquier nombre, pero se suele
elegir uno que describa la migración. Vea Introduction to migrations (Introducción a las migraciones) para
obtener más información.
El comando ef database update ejecuta el método Up en el archivo Migrations/<time-stamp>_InitialCreate.cs,
con lo que se crea la base de datos.
Aplicar scaffolding al modelo de película
Ejecute lo siguiente desde la línea de comandos (en el directorio del proyecto que contiene los archivos
Program.cs, Startup.cs y .csproj):

dotnet aspnet-codegenerator razorpage -m Movie -dc MovieContext -udl -outDir Pages/Movies --


referenceScriptLibraries

Si se produce un error:

No executable found matching command "dotnet-aspnet-codegenerator"

Abra un shell de comandos en el directorio del proyecto (el directorio que contiene los archivos Program.cs,
Startup.cs y .csproj).
Si se produce un error:
The process cannot access the file
'RazorPagesMovie/bin/Debug/netcoreapp2.0/RazorPagesMovie.dll'
because it is being used by another process.

Salga de Visual Studio y vuelva a ejecutar el comando.


En la tabla siguiente se incluyen los detalles de los parámetros de los generadores de código de ASP.NET Core:

PARÁMETRO DESCRIPTION

-m Nombre del modelo

-dc Contexto de datos

-udl Usa el diseño predeterminado.

-outDir Ruta de acceso relativa de la carpeta de salida para crear las


vistas

--referenceScriptLibraries Agrega _ValidationScriptsPartial a las páginas Editar y


Crear.

Active o desactive h para obtener ayuda con el comando aspnet-codegenerator razorpage :

dotnet aspnet-codegenerator razorpage -h

Probar la aplicación
Ejecute la aplicación y anexe /Movies a la dirección URL en el explorador ( http://localhost:port/movies ).
Pruebe el vínculo Crear.
Pruebe los vínculos Editar, Detalles y Eliminar.
Si recibe un error similar al siguiente, verifique que haya realizado las migraciones y que la base de datos esté
actualizada:

An unhandled exception occurred while processing the request.


'no such table: Movie'.

Agregar los archivos de página o película al proyecto


En Visual Studio, haga clic con el botón derecho en la carpeta Páginas y seleccione Agregar > Agregar
carpeta existente.
Seleccione la carpeta Películas.
En el cuadro de diálogo Elegir los archivos que se incluirán en el proyecto, seleccione Incluir todos.
En el tutorial siguiente se explican los archivos creados mediante scaffolding.

A N T E R IO R : S IG U IE N T E : P Á G IN A S D E R A Z O R C R E A D A S M E D IA N T E
IN T R O D U C C IÓ N S C A F F O L D IN G
Páginas de Razor con scaffolding en ASP.NET Core
25/06/2018 • 12 minutes to read • Edit Online

Por Rick Anderson


En este tutorial se examinan las páginas de Razor creadas por la técnica scaffolding en el tutorial anterior.
Vea o descargue un ejemplo.

Páginas Crear, Eliminar, Detalles y Editar.


Examine el modelo de página Pages/Movies/Index.cshtml.cs: [!code-csharp]
Las páginas de Razor se derivan de PageModel . Por convención, la clase derivada de PageModel se denomina
<PageName>Model . El constructor aplica la inserción de dependencias para agregar el MovieContext a la página.
Todas las páginas con scaffolding siguen este patrón. Vea Código asincrónico para obtener más información sobre
programación asincrónica con Entity Framework.
Cuando se efectúa una solicitud para la página, el método OnGetAsync devuelve una lista de películas a la página
de Razor. Se llama a OnGetAsync o a OnGet en una página de Razor para inicializar el estado de la página. En este
caso, OnGetAsync obtiene una lista de películas y las muestra.
Cuando OnGet devuelve void o OnGetAsync devuelve Task , no se utiliza ningún método de devolución. Cuando
el tipo de valor devuelto es IActionResult o Task<IActionResult> , se debe proporcionar una instrucción return.
Por ejemplo, el método Pages/Movies/Create.cshtml.cs OnPostAsync :

public async Task<IActionResult> OnPostAsync()


{
if (!ModelState.IsValid)
{
return Page();
}

_context.Movie.Add(Movie);
await _context.SaveChangesAsync();

return RedirectToPage("./Index");
}

Examine la página de Razor Pages/Movies/Index.cshtml:


@page
@model RazorPagesMovie.Pages.Movies.IndexModel

@{
ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
<a asp-page="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Movie[0].Title)
</th>
<th>
@Html.DisplayNameFor(model => model.Movie[0].ReleaseDate)
</th>
<th>
@Html.DisplayNameFor(model => model.Movie[0].Genre)
</th>
<th>
@Html.DisplayNameFor(model => model.Movie[0].Price)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Movie) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.ReleaseDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Genre)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
<a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>

Razor puede realizar la transición de HTML a C# o a un marcado específico de Razor. Cuando el símbolo @ va
seguido de una palabra clave reservada de Razor, realiza una transición a un marcado específico de Razor; en caso
contrario, realiza la transición a C#.
La directiva de Razor @page convierte el archivo en una acción — de MVC, lo que significa que puede controlar
las solicitudes. @page debe ser la primera directiva de Razor de una página. @page es un ejemplo de la transición
a un marcado específico de Razor. Vea Razor syntax (Sintaxis de Razor) para más información.
Examine la expresión lambda usada en la siguiente aplicación auxiliar HTML:
@Html.DisplayNameFor(model => model.Movie[0].Title))

La aplicación auxiliar HTML DisplayNameFor inspecciona la propiedad Title a la que se hace referencia en la
expresión lambda para determinar el nombre para mostrar. La expresión lambda se inspecciona, no se evalúa. Esto
significa que no hay ninguna infracción de acceso si model , model.Movie o model.Movie[0] son null o están
vacíos. Al evaluar la expresión lambda (por ejemplo, con @Html.DisplayFor(modelItem => item.Title) ), se evalúan
los valores de propiedad del modelo.
La directiva @model

@page
@model RazorPagesMovie.Pages.Movies.IndexModel

La directiva @model especifica el tipo del modelo que se pasa a la página de Razor. En el ejemplo anterior, la línea
@model permite que la clase derivada de PageModel esté disponible en la página de Razor. El modelo se usa en las
aplicaciones auxiliares HTML @Html.DisplayNameFor y @Html.DisplayName de la página.
Propiedades ViewData y Layout
Observe el código siguiente:

@page
@model RazorPagesMovie.Pages.Movies.IndexModel

@{
ViewData["Title"] = "Index";
}

El código resaltado anterior es un ejemplo de Razor con una transición a C#. Los caracteres { y } delimitan un
bloque de código de C#.
La clase base PageModel tiene una propiedad de diccionario ViewData que se puede usar para agregar datos que
quiera pasar a una vista. Puede agregar objetos al diccionario ViewData con un patrón de clave/valor. En el
ejemplo anterior, se agrega la propiedad "Title" al diccionario ViewData . La propiedad "Title" se usa en el archivo
Pages/_Layout.cshtml. En el siguiente marcado se muestran las primeras líneas del archivo Pages/_Layout.cshtml.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - RazorPagesMovie</title>

@*Markup removed for brevity.*@

La línea @*Markup removed for brevity.*@ es un comentario de Razor. A diferencia de los comentarios HTML (
<!-- --> ), los comentarios de Razor no se envían al cliente.

Ejecute la aplicación y pruebe los vínculos del proyecto (Inicio, Acerca de, Contacto, Crear, Editar y Eliminar).
Cada página establece el título, que puede ver en la pestaña del explorador. Al marcar una página, se usa el título
para el marcador. Pages/Index.cshtml y Pages/Movies/Index.cshtml actualmente tienen el mismo título, pero
puede modificarlos para que tengan valores diferentes.
La propiedad Layout se establece en el archivo Pages/_ViewStart.cshtml:
@{
Layout = "_Layout";
}

El marcado anterior establece el archivo de diseño en Pages/_Layout.cshtml para todos los archivos de Razor en la
carpeta Pages. Vea Layout (Diseño) para más información.
Actualizar el diseño
Cambie el elemento <title> del archivo Pages/_Layout.cshtml para usar una cadena más corta.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - Movie</title>

Busque el siguiente elemento delimitador en el archivo Pages/_Layout.cshtml.

<a asp-page="/Index" class="navbar-brand">RazorPagesMovie</a>

Reemplace el elemento anterior por el marcado siguiente.

<a asp-page="/Movies/Index" class="navbar-brand">RpMovie</a>

El elemento delimitador anterior es una aplicación auxiliar de etiquetas. En este caso, se trata de la aplicación
auxiliar de etiquetas Anchor. El atributo y valor de la aplicación auxiliar de etiquetas asp-page="/Movies/Index"
crea un vínculo a la página de Razor /Movies/Index .
Guarde los cambios y pruebe la aplicación haciendo clic en el vínculo RpMovie. Consulte el archivo
_Layout.cshtml en GitHub.
Modelo de página Crear
Examine el modelo de página Pages/Movies/Create.cshtml.cs:
// Unused usings removed.
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesMovie.Models;
using System.Threading.Tasks;

namespace RazorPagesMovie.Pages.Movies
{
public class CreateModel : PageModel
{
private readonly RazorPagesMovie.Models.MovieContext _context;

public CreateModel(RazorPagesMovie.Models.MovieContext context)


{
_context = context;
}

public IActionResult OnGet()


{
return Page();
}

[BindProperty]
public Movie Movie { get; set; }

public async Task<IActionResult> OnPostAsync()


{
if (!ModelState.IsValid)
{
return Page();
}

_context.Movie.Add(Movie);
await _context.SaveChangesAsync();

return RedirectToPage("./Index");
}
}
}

El método OnGet inicializa cualquier estado necesario para la página. La página Crear no tiene ningún estado para
inicializar. El método Page crea un objeto PageResult que representa la página Create.cshtml.
La propiedad Movie usa el atributo [BindProperty] para participar en el enlace de modelos. Cuando el
formulario de creación publica los valores del formulario, el tiempo de ejecución de ASP.NET Core enlaza los
valores publicados con el modelo Movie .
El método OnPostAsync se ejecuta cuando la página publica los datos del formulario:

public async Task<IActionResult> OnPostAsync()


{
if (!ModelState.IsValid)
{
return Page();
}

_context.Movie.Add(Movie);
await _context.SaveChangesAsync();

return RedirectToPage("./Index");
}

Si hay algún error de modelo, se vuelve a mostrar el formulario, junto con los datos del formulario publicados. La
mayoría de los errores de modelo se pueden capturar en el cliente antes de que se publique el formulario. Un
ejemplo de un error de modelo consiste en publicar un valor para el campo de fecha que no se puede convertir en
una fecha. Más adelante en el tutorial volveremos a hablar de la validación del lado cliente y de la validación de
modelos.
Si no hay ningún error de modelo, los datos se guardan y el explorador se redirige a la página Índice.
Página de Razor Crear
Examine el archivo de la página de Razor Pages/Movies/Create.cshtml:

@page
@model RazorPagesMovie.Pages.Movies.CreateModel

@{
ViewData["Title"] = "Create";
}

<h2>Create</h2>

<h4>Movie</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Movie.Title" class="control-label"></label>
<input asp-for="Movie.Title" class="form-control" />
<span asp-validation-for="Movie.Title" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Movie.ReleaseDate" class="control-label"></label>
<input asp-for="Movie.ReleaseDate" class="form-control" />
<span asp-validation-for="Movie.ReleaseDate" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Movie.Genre" class="control-label"></label>
<input asp-for="Movie.Genre" class="form-control" />
<span asp-validation-for="Movie.Genre" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Movie.Price" class="control-label"></label>
<input asp-for="Movie.Price" class="form-control" />
<span asp-validation-for="Movie.Price" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</form>
</div>
</div>

<div>
<a asp-page="Index">Back to List</a>
</div>

@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

El elemento <form method="post"> es una aplicación auxiliar de etiquetas de formulario. La aplicación auxiliar de
etiquetas de formulario incluye automáticamente un token antifalsificación.
El motor de scaffolding crea un marcado de Razor para cada campo del modelo (excepto el identificador) similar
al siguiente:

<div asp-validation-summary="ModelOnly" class="text-danger"></div>


<div class="form-group">
<label asp-for="Movie.Title" class="control-label"></label>
<input asp-for="Movie.Title" class="form-control" />
<span asp-validation-for="Movie.Title" class="text-danger"></span>
</div>

Las aplicaciones auxiliares de etiquetas de validación ( <div asp-validation-summary y <span asp-validation-for )


muestran errores de validación. La validación se trata con más detalle en un punto posterior de esta serie.
La aplicación auxiliar de etiquetas ( <label asp-for="Movie.Title" class="control-label"></label> ) genera el título
de la etiqueta y el atributo for para la propiedad Title .
La aplicación auxiliar de etiquetas de entrada ( <input asp-for="Movie.Title" class="form-control" /> ) usa los
atributos DataAnnotations y genera los atributos HTML necesarios para la validación de jQuery en el lado del
cliente.
En el siguiente tutorial se describe SQLite y la propagación de la base de datos.

A N T E R IO R : A D IC IÓ N D E U N S IG U IE N T E :
M ODELO S Q L IT E
Trabajar con SQLite y páginas de Razor
25/06/2018 • 2 minutes to read • Edit Online

Por Rick Anderson


El objeto MovieContext controla la tarea de conexión a la base de datos y asignación de objetos Movie a los
registros de la base de datos. El contexto de base de datos se registra con el contenedor de inserción de
dependencias en el método ConfigureServices del archivo Startup.cs:

public void ConfigureServices(IServiceCollection services)


{
// requires
// using RazorPagesMovie.Models;
// using Microsoft.EntityFrameworkCore;

services.AddDbContext<MovieContext>(options =>
options.UseSqlite(Configuration.GetConnectionString("MovieContext")));
services.AddMvc();
}

SQLite
Según la información del sitio web de SQLite:

SQLite es un motor de base de datos SQL independiente, de alta confiabilidad, insertado, con características
completas y dominio público. SQLite es el motor de base de datos más usado en el mundo.

Existen muchas herramientas de terceros que se pueden descargar para administrar y ver una base de datos de
SQLite. La imagen de abajo es de DB Browser for SQLite. Si tiene una herramienta favorita de SQLite, deje un
comentario sobre lo que le gusta de ella.
Inicializar la base de datos
Cree una nueva clase denominada SeedData en la carpeta Models. Reemplace el código generado con el
siguiente:
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Linq;

namespace RazorPagesMovie.Models
{
public static class SeedData
{
public static void Initialize(IServiceProvider serviceProvider)
{
using (var context = new MovieContext(
serviceProvider.GetRequiredService<DbContextOptions<MovieContext>>()))
{
// Look for any movies.
if (context.Movie.Any())
{
return; // DB has been seeded
}

context.Movie.AddRange(
new Movie
{
Title = "When Harry Met Sally",
ReleaseDate = DateTime.Parse("1989-2-12"),
Genre = "Romantic Comedy",
Price = 7.99M
},

new Movie
{
Title = "Ghostbusters",
ReleaseDate = DateTime.Parse("1984-3-13"),
Genre = "Comedy",
Price = 8.99M
},

new Movie
{
Title = "Ghostbusters 2",
ReleaseDate = DateTime.Parse("1986-2-23"),
Genre = "Comedy",
Price = 9.99M
},

new Movie
{
Title = "Rio Bravo",
ReleaseDate = DateTime.Parse("1959-4-15"),
Genre = "Western",
Price = 3.99M
}
);
context.SaveChanges();
}
}
}
}

Si hay alguna película en la base de datos, se devuelve el inicializador.


if (context.Movie.Any())
{
return; // DB has been seeded.
}

Agregar el inicializador
Agregue el inicializador al método Main en el archivo Program.cs:

// Unused usings removed.


using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using RazorPagesMovie.Models;
using System;
using Microsoft.EntityFrameworkCore;

namespace RazorPagesMovie
{
public class Program
{
public static void Main(string[] args)
{
var host = BuildWebHost(args);

using (var scope = host.Services.CreateScope())


{
var services = scope.ServiceProvider;

try
{
var context = services.GetRequiredService<MovieContext>();
// requires using Microsoft.EntityFrameworkCore;
context.Database.Migrate();
// Requires using RazorPagesMovie.Models;
SeedData.Initialize(services);
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred seeding the DB.");
}
}

host.Run();
}

public static IWebHost BuildWebHost(string[] args) =>


WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.Build();
}
}

Prueba de la aplicación
Elimine todos los registros de la base de datos (para que se ejecute el método de inicialización). Detenga e inicie la
aplicación para inicializar la base de datos.
La aplicación muestra los datos inicializados.
A N T E R IO R : A D IC IÓ N D E U N S IG U IE N T E : A C T U A L IZ A C IÓ N D E
M ODELO P Á G IN A S
Actualización de las páginas generadas
25/06/2018 • 7 minutes to read • Edit Online

Por Rick Anderson


La aplicación de películas pinta bien, pero la presentación no es ideal. No queremos ver la hora (12:00:00 a.m. en
la imagen siguiente) y ReleaseDate debe ser Release Date (dos palabras).

Actualización del código generado


Abra el archivo Models/Movie.cs y agregue las líneas resaltadas mostradas en el código siguiente:

using System;
using System.ComponentModel.DataAnnotations;

namespace RazorPagesMovie.Models
{
public class Movie
{
public int ID { get; set; }
public string Title { get; set; }

[Display(Name = "Release Date")]


[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }
public string Genre { get; set; }
public decimal Price { get; set; }
}
}
En el próximo tutorial hablaremos de DataAnnotations. El atributo Display especifica qué se muestra como
nombre de un campo (en este caso, "Release Date" en lugar de "ReleaseDate"). El atributo DataType especifica el
tipo de los datos (Date), así que la información de hora almacenada en el campo no se muestra.
Vaya a Pages/Movies y mantenga el mouse sobre un vínculo de edición para ver la dirección URL de destino.

Los vínculos Edit, Details y Delete son generados por la aplicación auxiliar de etiquetas de delimitador del
archivo Pages/Movies/Index.cshtml.

@foreach (var item in Model.Movie) {


<tr>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.ReleaseDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Genre)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
<a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>

Las aplicaciones auxiliares de etiquetas permiten que el código de servidor participe en la creación y la
representación de elementos HTML en archivos de Razor. En el código anterior, AnchorTagHelper genera de forma
dinámica el valor del atributo href HTML desde la página de Razor (la ruta es relativa), el elemento asp-page y
el identificador de ruta ( asp-route-id ). Vea Generación de direcciones URL para las páginas para obtener más
información.
Use Ver código fuente en su explorador preferido para examinar el marcado generado. A continuación se
muestra una parte del HTML generado:

<td>
<a href="/Movies/Edit?id=1">Edit</a> |
<a href="/Movies/Details?id=1">Details</a> |
<a href="/Movies/Delete?id=1">Delete</a>
</td>

Los vínculos generados de forma dinámica pasan el identificador de la película con una cadena de consulta (por
ejemplo, http://localhost:5000/Movies/Details?id=2 ).
Actualice las páginas de edición, detalles y eliminación de Razor para usar la plantilla de ruta "{id:int}". Cambie la
directiva de página de cada una de estas páginas de @page a @page "{id:int}" . Ejecute la aplicación y luego vea
el origen. El HTML generado agrega el identificador a la parte de la ruta de acceso de la dirección URL:

<td>
<a href="/Movies/Edit/1">Edit</a> |
<a href="/Movies/Details/1">Details</a> |
<a href="/Movies/Delete/1">Delete</a>
</td>

Una solicitud a la página con la plantilla de ruta "{id:int}" que no incluya el entero devolverá un error HTTP 404
(no encontrado). Por ejemplo, http://localhost:5000/Movies/Details devolverá un error 404. Para que el
identificador sea opcional, anexe ? a la restricción de ruta:

@page "{id:int?}"

Actualización del control de excepciones de simultaneidad


Actualice el método OnPostAsync en el archivo Pages/Movies/Edit.cshtml.cs. En el código resaltado siguiente se
muestran los cambios:
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}

_context.Attach(Movie).State = EntityState.Modified;

try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!_context.Movie.Any(e => e.ID == Movie.ID))
{
return NotFound();
}
else
{
throw;
}
}

return RedirectToPage("./Index");
}

El código anterior solo detecta las excepciones de simultaneidad cuando el primer cliente simultáneo elimina la
película y el segundo cliente simultáneo publica cambios en ella.
Para probar el bloque catch :
Establecer un punto de interrupción en catch (DbUpdateConcurrencyException)
Edite una película.
En otra ventana del explorador, seleccione el vínculo de eliminación de la misma película y luego elimínela.
En la ventana anterior del explorador, publique los cambios en la película.
El código de producción por lo general debería detectar conflictos de simultaneidad cuando dos o más clientes
actualizan un registro de forma simultánea. Vea Administración de conflictos de simultaneidad para más
información.
Revisión de publicaciones y enlaces
Examine el archivo Pages/Movies/Edit.cshtml.cs: [!code-csharp]
Cuando se realiza una solicitud HTTP GET a la página Movies/Edit (por ejemplo,
http://localhost:5000/Movies/Edit/2 ):

El método OnGetAsync obtiene la película en la base de datos y devuelve el método Page .


El método Page presenta la página de Razor Pages/Movies/Edit.cshtml. El archivo Pages/Movies/Edit.cshtml
contiene la directiva de modelo ( @model RazorPagesMovie.Pages.Movies.EditModel ), que hace que el modelo de
película esté disponible en la página.
Se abre el formulario de edición con los valores de la película.
Cuando se publica la página Movies/Edit:
Los valores del formulario de la página se enlazan a la propiedad Movie . El atributo [BindProperty]
habilita el enlace de modelos.
[BindProperty]
public Movie Movie { get; set; }

Si hay errores en el estado del modelo (por ejemplo, ReleaseDate no se puede convertir en una fecha), el
formulario se vuelve a publicar con los valores enviados.
Si no hay ningún error en el modelo, se guarda la película.
Los métodos HTTP GET de las páginas de índice, creación y eliminación de Razor siguen un patrón similar. El
método HTTP POST OnPostAsync de la página de creación de Razor sigue un patrón similar al del método
OnPostAsync de la página de edición de Razor.

La búsqueda se incluye en el tutorial siguiente.

A N T E R IO R : T R A B A J A R C O N A GREGA R
S Q L IT E BÚSQUEDA
Adición de búsqueda a la aplicación páginas de
Razor
25/06/2018 • 6 minutes to read • Edit Online

Por Rick Anderson


En este documento se agrega a la página de índice una capacidad de búsqueda que permite buscar películas por
género o nombre.
Actualice el método OnGetAsync de la página de índice con el código siguiente:

@{
Layout = "_Layout";
}

public async Task OnGetAsync(string searchString)


{
var movies = from m in _context.Movie
select m;

if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}

Movie = await movies.ToListAsync();


}

La primera línea del método OnGetAsync crea una consulta LINQ para seleccionar las películas:

var movies = from m in _context.Movie


select m;

En este momento solo se define la consulta, no se ejecuta en la base de datos.


Si el parámetro searchString contiene una cadena, la consulta de películas se modifica para filtrar según la
cadena de búsqueda:

if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}

El código s => s.Title.Contains() es una expresión lambda. Las lambdas se usan en consultas LINQ basadas en
métodos como argumentos para métodos de operador de consulta estándar, como el método Where o Contains
(usado en el código anterior). Las consultas LINQ no se ejecutan cuando se definen ni cuando se modifican
mediante una llamada a un método (como Where , Contains u OrderBy ). En su lugar, se aplaza la ejecución de la
consulta. Esto significa que la evaluación de una expresión se aplaza hasta que su valor realizado se repite o se
llama al método ToListAsync . Para más información, vea Query Execution (Ejecución de consultas).
Nota: El método Contains se ejecuta en la base de datos, no en el código de C#. La distinción entre mayúsculas y
minúsculas en la consulta depende de la base de datos y la intercalación. En SQL Server, Contains se asigna a
SQL LIKE, que distingue entre mayúsculas y minúsculas. En SQLite, con la intercalación predeterminada, se
distingue entre mayúsculas y minúsculas.
Vaya a la página de películas y anexe una cadena de consulta como ?searchString=Ghost a la dirección URL (por
ejemplo, http://localhost:5000/Movies?searchString=Ghost ). Se muestran las películas filtradas.

Si se agrega la siguiente plantilla de ruta a la página de índice, la cadena de búsqueda se puede pasar como un
segmento de dirección URL (por ejemplo, http://localhost:5000/Movies/ghost ).

@page "{searchString?}"

La restricción de ruta anterior permite buscar el título como datos de ruta (un segmento de dirección URL ) en
lugar de como un valor de cadena de consulta. El elemento ? de "{searchString?}" significa que se trata de un
parámetro de ruta opcional.
Sin embargo, no se puede esperar que los usuarios modifiquen la dirección URL para buscar una película. En este
paso, se agrega la interfaz de usuario para filtrar las películas. Si ha agregado la restricción de ruta
"{searchString?}" , quítela.

Abra el archivo Pages/Movies/Index.cshtml y agregue el marcado <form> resaltado en el siguiente código:

@page
@model RazorPagesMovie.Pages.Movies.IndexModel

@{
ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
<a asp-page="Create">Create New</a>
</p>

<form>
<p>
Title: <input type="text" name="SearchString">
<input type="submit" value="Filter" />
</p>
</form>

<table class="table">
@*Markup removed for brevity.*@

La etiqueta HTML <form> usa la aplicación auxiliar de etiquetas de formulario. Cuando se envía el formulario, la
cadena de filtro se envía a la página Pages/Movies/Index. Guarde los cambios y pruebe el filtro.

Búsqueda por género


Agregue las siguientes propiedades resaltadas a Pages/Movies/Index.cshtml.cs:
public class IndexModel : PageModel
{
private readonly RazorPagesMovie.Models.MovieContext _context;

public IndexModel(RazorPagesMovie.Models.MovieContext context)


{
_context = context;
}

public IList<Movie> Movie { get; set; }


public SelectList Genres { get; set; }
public string MovieGenre { get; set; }

El elemento SelectList Genres contiene la lista de géneros. Esto permite al usuario seleccionar un género de la
lista.
La propiedad MovieGenre contiene el género concreto que selecciona el usuario (por ejemplo, "Western").
Actualice el método OnGetAsync con el código siguiente:

// Requires using Microsoft.AspNetCore.Mvc.Rendering;


public async Task OnGetAsync(string movieGenre, string searchString)
{
// Use LINQ to get list of genres.
IQueryable<string> genreQuery = from m in _context.Movie
orderby m.Genre
select m.Genre;

var movies = from m in _context.Movie


select m;

if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}

if (!String.IsNullOrEmpty(movieGenre))
{
movies = movies.Where(x => x.Genre == movieGenre);
}
Genres = new SelectList(await genreQuery.Distinct().ToListAsync());
Movie = await movies.ToListAsync();
}

El código siguiente es una consulta LINQ que recupera todos los géneros de la base de datos.

// Use LINQ to get list of genres.


IQueryable<string> genreQuery = from m in _context.Movie
orderby m.Genre
select m.Genre;

La SelectList de géneros se crea mediante la proyección de los distintos géneros.

Genres = new SelectList(await genreQuery.Distinct().ToListAsync());

Adición de búsqueda por género


Actualice Index.cshtml como se indica a continuación:
@page
@model RazorPagesMovie.Pages.Movies.IndexModel

@{
ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
<a asp-page="Create">Create New</a>
</p>

<form>
<p>
<select asp-for="MovieGenre" asp-items="Model.Genres">
<option value="">All</option>
</select>

Title: <input type="text" name="SearchString">


<input type="submit" value="Filter" />
</p>
</form>

<table class="table">
<thead>

Pruebe la aplicación al buscar por género, título de la película y ambos.

A N T E R IO R : A C T U A L IZ A C IÓ N D E S IG U IE N T E : A D IC IÓ N D E U N N U E V O
P Á G IN A S CAM PO
Creación de una aplicación web de páginas de Razor
con ASP.NET Core y Visual Studio Code
21/06/2018 • 2 minutes to read • Edit Online

Se está trabajando en este artículo.


En esta serie se explican los conceptos básicos de la creación de una aplicación web de páginas de Razor con
ASP.NET Core mediante Visual Studio Code.
1. Introducción a las páginas de Razor con VS Code
2. Adición de un modelo a una aplicación de páginas de Razor
3. Páginas de Razor con scaffolding
4. Trabajar con SQLite
5. Actualización de las páginas
6. Agregar búsqueda
Hasta que se complete la siguiente sección, siga la versión de Visual Studio para Windows.
1. Agregar un campo nuevo
2. Agregar validación
Introducción a las páginas de Razor en ASP.NET Core
con Visual Studio Code
11/06/2018 • 5 minutes to read • Edit Online

Por Rick Anderson


En este tutorial se enseñan los conceptos básicos de la compilación de una aplicación web de páginas de Razor de
ASP.NET Core. Se recomienda leer Introduction to Razor Pages (Introducción a las páginas de Razor) antes de
empezar este tutorial. Las páginas de Razor son el método recomendado para crear la interfaz de usuario de
aplicaciones web en ASP.NET Core.

Requisitos previos
Install the following:
.NET Core SDK 2.0 or later
Visual Studio Code
C# for Visual Studio Code
.NET Core 2.1 SDK or later
Visual Studio Code
C# for Visual Studio Code

Creación de una aplicación web de Razor


Desde un terminal, ejecute estos comandos:

dotnet new webapp -o RazorPagesMovie


cd RazorPagesMovie
dotnet run

NOTE
In ASP.NET Core 2.1 or later, webapp is an alias of the razor argument. If the dotnet new webapp <OPTIONS> command
loads the dotnet new command help instead of creating a new Razor Pages app, install the .NET Core 2.1 SDK.

dotnet new razor -o RazorPagesMovie


cd RazorPagesMovie
dotnet run

Los comandos anteriores usan el CLI de .NET Core para crear y ejecutar un proyecto de páginas de Razor. Abra
http://localhost:5000 en un explorador para ver la aplicación.
La plantilla predeterminada crea los vínculos y las páginas RazorPagesMovie, Inicio, Sobre y Contacto. Según
el tamaño de la ventana del explorador, tendrá que hacer clic en el icono de navegación para mostrar los vínculos.

Pruebe los vínculos. Los vínculos RazorPagesMovie e Inicio dirigen a la página de índice. Los vínculos Acerca
de y Contacto dirigen a las páginas About y Contact respectivamente.

Archivos y carpetas del proyecto


En la tabla siguiente se enumeran los archivos y las carpetas del proyecto. En este tutorial, el archivo Startup.cs es
el más importante. No es necesario revisar todos los vínculos siguientes. Los vínculos se proporcionan como
referencia para cuando necesite más información sobre un archivo o una carpeta del proyecto.

ARCHIVO O CARPETA PROPÓSITO

wwwroot Contiene archivos estáticos. Vea Archivos estáticos.

Páginas Carpeta para páginas de Razor.

appsettings.json Configuración

Program.cs Aloja la aplicación de ASP.NET Core.

Startup.cs Configura los servicios y la canalización de solicitudes.


Consulte Inicio.

Carpeta Páginas
El archivo _Layout.cshtml contiene elementos HTML comunes (scripts y hojas de estilos) y establece el diseño de
la aplicación. Por ejemplo, al hacer clic en RazorPagesMovie, Inicio, Acerca de o Contacto, verá los mismos
elementos. Los elementos comunes incluyen el menú de navegación de la parte superior y el encabezado de la
parte inferior de la ventana. Consulte Diseño para obtener más información.
El archivo _ViewStart.cshtml establece la propiedad Layout de las páginas de Razor que para usar el archivo
_Layout.cshtml. Vea Layout (Diseño) para más información.
El archivo _ViewImports.cshtml contiene directivas de Razor que se importan en cada página de Razor. Consulte
Importing Shared Directives (Importar directivas compartidas) para obtener más información.
El archivo _ValidationScriptsPartial.cshtml proporciona una referencia de los scripts de validación de jQuery. Al
agregar las páginas Create y Edit más adelante en el tutorial, se usará el archivo
_ValidationScriptsPartial.cshtml.
Las páginas About , Contact y Index son páginas básicas que puede usar para iniciar una aplicación. La página
Error se usa para mostrar información de errores.

Abrir el proyecto
Presione CTRL+C para cerrar la aplicación.
En Visual Studio Code (VS Code), seleccione Archivo > Abrir carpeta y elija la carpeta RazorPagesMovie.
Seleccione Sí en el mensaje de advertencia "Required assets to build and debug are missing from
'RazorPagesMovie'. Add them?" (Faltan los activos necesarios para compilar y depurar en 'RazorPagesMovie'.
Add them?" (Faltan los activos necesarios para compilar y depurar en 'TodoApi'. ¿Quiere agregarlos?).
Seleccione Restaurar en el mensaje de información "Hay dependencias no resueltas".
Iniciar la aplicación
Presione Ctrl+F5 para iniciar la aplicación sin depurar. Si lo prefiere, puede ir al menú Depurar y seleccionar
Iniciar sin depurar.
En el tutorial siguiente, agregaremos un modelo al proyecto.
S IG U IE N T E : A D IC IÓ N D E U N
M ODELO
Agregar un modelo a una aplicación de páginas de
Razor de ASP.NET Core con Visual Studio Code
25/06/2018 • 7 minutes to read • Edit Online

Por Rick Anderson


En esta sección, agregará las clases para administrar películas en una base de datos. Estas clases se usan con
Entity Framework Core (EF Core) para trabajar con una base de datos. EF Core es un marco de trabajo de
asignación relacional de objetos (ORM ) que simplifica el código de acceso de datos que se debe escribir.
Las clases de modelo que se crean se conocen como clases POCO (del inglés "plain-old CLR objects", objetos
CLR antiguos sin formato) porque no tienen ninguna dependencia de EF Core. Definen las propiedades de los
datos que se almacenan en la base de datos.
En este tutorial, se escriben primero las clases del modelo y EF Core crea la base de datos. Hay un enfoque
alternativo, que no se trata aquí, que consiste en generar clases del modelo a partir de una base de datos existente.
Vea o descargue un ejemplo.

Agregar un modelo de datos


Agregue una carpeta denominada Modelos.
Agregue una clase a la carpeta Modelos denominada Movie.cs.
Agregue el código siguiente al archivo Modelos/Movie.cs:
Agregue las propiedades siguientes a la clase Movie :

using System;

namespace RazorPagesMovie.Models
{
public class Movie
{
public int ID { get; set; }
public string Title { get; set; }
public DateTime ReleaseDate { get; set; }
public string Genre { get; set; }
public decimal Price { get; set; }
}
}

La base de datos requiere el campo ID para la clave principal.


Agregar una clase de contexto de base de datos
Agregue la clase MovieContext.cs siguiente a la carpeta Models:
using Microsoft.EntityFrameworkCore;

namespace RazorPagesMovie.Models
{
public class MovieContext : DbContext
{
public MovieContext(DbContextOptions<MovieContext> options)
: base(options)
{
}

public DbSet<Movie> Movie { get; set; }


}
}

El código anterior crea una propiedad DbSet para el conjunto de entidades. En la terminología de Entity
Framework, un conjunto de entidades suele corresponderse con una tabla de base de datos, mientras que una
entidad lo hace con una fila de la tabla.
Agregar una cadena de conexión de base de datos
Agregue una cadena de conexión al archivo appsettings.json.

{
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Warning"
}
},
"ConnectionStrings": {
"MovieContext": "Data Source=MvcMovie.db"
}
}

Registrar el contexto de base de datos


Registre el contexto de base de datos con el contenedor de inserción de dependencias en el archivo Startup.cs.

public void ConfigureServices(IServiceCollection services)


{
// requires
// using RazorPagesMovie.Models;
// using Microsoft.EntityFrameworkCore;

services.AddDbContext<MovieContext>(options =>
options.UseSqlite(Configuration.GetConnectionString("MovieContext")));
services.AddMvc();
}

Compile el proyecto para comprobar que no contiene errores.


Paquetes de Entity Framework Core NuGet para migraciones
Las herramientas de EF para la interfaz de nivel de la línea de comandos se proporcionan en
Microsoft.EntityFrameworkCore.Tools.DotNet. Para instalar este paquete, agréguelo a la colección
DotNetCliToolReference del archivo .csproj. Nota: Tendrá que instalar este paquete mediante la edición del archivo
.csproj; no se puede usar el comando install-package ni la interfaz gráfica de usuario del administrador de
paquetes.
Edite el archivo RazorPagesMovie.csproj:
Seleccione Archivo > Abrir archivo y, después, el archivo RazorPagesMovie.csproj.
Agregue la referencia de herramientas de Microsoft.EntityFrameworkCore.Tools.DotNet al segundo
<ItemGroup>:

<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.3" />
</ItemGroup>
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.2" />
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.1" />
</ItemGroup>
</Project>

Agregar herramientas de scaffolding y realizar la migración inicial


Desde la línea de comandos, ejecute los comandos CLI de .NET Core:

dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design


dotnet restore
dotnet ef migrations add InitialCreate
dotnet ef database update

El comando add package instala las herramientas necesarias para ejecutar el motor de scaffolding.
El comando ef migrations add InitialCreate genera el código para crear el esquema de base de datos inicial. El
esquema se basa en el modelo especificado en DbContext (en el archivo Models/MovieContext.cs). El argumento
InitialCreate se usa para asignar un nombre a las migraciones. Puede usar cualquier nombre, pero se suele
elegir uno que describa la migración. Vea Introduction to migrations (Introducción a las migraciones) para
obtener más información.
El comando ef database update ejecuta el método Up en el archivo Migrations/<time-stamp>_InitialCreate.cs,
con lo que se crea la base de datos.
Aplicar scaffolding al modelo de película
Abra una ventana de comandos en el directorio del proyecto (el directorio que contiene los archivos
Program.cs, Startup.cs y .csproj).
Ejecute el siguiente comando:
Nota: ejecute el siguiente comando en Windows. Para MacOS y Linux, vea el siguiente comando

dotnet aspnet-codegenerator razorpage -m Movie -dc MovieContext -udl -outDir Pages\Movies --


referenceScriptLibraries

En MacOS y Linux, ejecute el siguiente comando:

dotnet aspnet-codegenerator razorpage -m Movie -dc MovieContext -udl -outDir Pages/Movies --


referenceScriptLibraries

Si se produce un error:
The process cannot access the file
'RazorPagesMovie/bin/Debug/netcoreapp2.0/RazorPagesMovie.dll'
because it is being used by another process.

Salga de Visual Studio y vuelva a ejecutar el comando.


En la tabla siguiente se incluyen los detalles de los parámetros de los generadores de código de ASP.NET Core:

PARÁMETRO DESCRIPTION

-m Nombre del modelo

-dc Contexto de datos

-udl Usa el diseño predeterminado.

-outDir Ruta de acceso relativa de la carpeta de salida para crear las


vistas

--referenceScriptLibraries Agrega _ValidationScriptsPartial a las páginas Editar y


Crear.

Active o desactive h para obtener ayuda con el comando aspnet-codegenerator razorpage :

dotnet aspnet-codegenerator razorpage -h

Probar la aplicación
Ejecute la aplicación y anexe /Movies a la dirección URL en el explorador ( http://localhost:port/movies ).
Pruebe el vínculo Crear.
Pruebe los vínculos Editar, Detalles y Eliminar.
Si recibe un error similar al siguiente, verifique que haya realizado las migraciones y que la base de datos esté
actualizada:

An unhandled exception occurred while processing the request.


'no such table: Movie'.

En el tutorial siguiente se explican los archivos creados mediante scaffolding.

A N T E R IO R : S IG U IE N T E : P Á G IN A S D E R A Z O R C R E A D A S M E D IA N T E
IN T R O D U C C IÓ N S C A F F O L D IN G
Páginas de Razor con scaffolding en ASP.NET Core
25/06/2018 • 12 minutes to read • Edit Online

Por Rick Anderson


En este tutorial se examinan las páginas de Razor creadas por la técnica scaffolding en el tutorial anterior.
Vea o descargue un ejemplo.

Páginas Crear, Eliminar, Detalles y Editar.


Examine el modelo de página Pages/Movies/Index.cshtml.cs: [!code-csharp]
Las páginas de Razor se derivan de PageModel . Por convención, la clase derivada de PageModel se denomina
<PageName>Model . El constructor aplica la inserción de dependencias para agregar el MovieContext a la página.
Todas las páginas con scaffolding siguen este patrón. Vea Código asincrónico para obtener más información sobre
programación asincrónica con Entity Framework.
Cuando se efectúa una solicitud para la página, el método OnGetAsync devuelve una lista de películas a la página
de Razor. Se llama a OnGetAsync o a OnGet en una página de Razor para inicializar el estado de la página. En este
caso, OnGetAsync obtiene una lista de películas y las muestra.
Cuando OnGet devuelve void o OnGetAsync devuelve Task , no se utiliza ningún método de devolución. Cuando
el tipo de valor devuelto es IActionResult o Task<IActionResult> , se debe proporcionar una instrucción return.
Por ejemplo, el método Pages/Movies/Create.cshtml.cs OnPostAsync :

public async Task<IActionResult> OnPostAsync()


{
if (!ModelState.IsValid)
{
return Page();
}

_context.Movie.Add(Movie);
await _context.SaveChangesAsync();

return RedirectToPage("./Index");
}

Examine la página de Razor Pages/Movies/Index.cshtml:


@page
@model RazorPagesMovie.Pages.Movies.IndexModel

@{
ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
<a asp-page="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Movie[0].Title)
</th>
<th>
@Html.DisplayNameFor(model => model.Movie[0].ReleaseDate)
</th>
<th>
@Html.DisplayNameFor(model => model.Movie[0].Genre)
</th>
<th>
@Html.DisplayNameFor(model => model.Movie[0].Price)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Movie) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.ReleaseDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Genre)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
<a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>

Razor puede realizar la transición de HTML a C# o a un marcado específico de Razor. Cuando el símbolo @ va
seguido de una palabra clave reservada de Razor, realiza una transición a un marcado específico de Razor; en caso
contrario, realiza la transición a C#.
La directiva de Razor @page convierte el archivo en una acción — de MVC, lo que significa que puede controlar
las solicitudes. @page debe ser la primera directiva de Razor de una página. @page es un ejemplo de la transición
a un marcado específico de Razor. Vea Razor syntax (Sintaxis de Razor) para más información.
Examine la expresión lambda usada en la siguiente aplicación auxiliar HTML:
@Html.DisplayNameFor(model => model.Movie[0].Title))

La aplicación auxiliar HTML DisplayNameFor inspecciona la propiedad Title a la que se hace referencia en la
expresión lambda para determinar el nombre para mostrar. La expresión lambda se inspecciona, no se evalúa. Esto
significa que no hay ninguna infracción de acceso si model , model.Movie o model.Movie[0] son null o están
vacíos. Al evaluar la expresión lambda (por ejemplo, con @Html.DisplayFor(modelItem => item.Title) ), se evalúan
los valores de propiedad del modelo.
La directiva @model

@page
@model RazorPagesMovie.Pages.Movies.IndexModel

La directiva @model especifica el tipo del modelo que se pasa a la página de Razor. En el ejemplo anterior, la línea
@model permite que la clase derivada de PageModel esté disponible en la página de Razor. El modelo se usa en las
aplicaciones auxiliares HTML @Html.DisplayNameFor y @Html.DisplayName de la página.
Propiedades ViewData y Layout
Observe el código siguiente:

@page
@model RazorPagesMovie.Pages.Movies.IndexModel

@{
ViewData["Title"] = "Index";
}

El código resaltado anterior es un ejemplo de Razor con una transición a C#. Los caracteres { y } delimitan un
bloque de código de C#.
La clase base PageModel tiene una propiedad de diccionario ViewData que se puede usar para agregar datos que
quiera pasar a una vista. Puede agregar objetos al diccionario ViewData con un patrón de clave/valor. En el
ejemplo anterior, se agrega la propiedad "Title" al diccionario ViewData . La propiedad "Title" se usa en el archivo
Pages/_Layout.cshtml. En el siguiente marcado se muestran las primeras líneas del archivo Pages/_Layout.cshtml.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - RazorPagesMovie</title>

@*Markup removed for brevity.*@

La línea @*Markup removed for brevity.*@ es un comentario de Razor. A diferencia de los comentarios HTML (
<!-- --> ), los comentarios de Razor no se envían al cliente.

Ejecute la aplicación y pruebe los vínculos del proyecto (Inicio, Acerca de, Contacto, Crear, Editar y Eliminar).
Cada página establece el título, que puede ver en la pestaña del explorador. Al marcar una página, se usa el título
para el marcador. Pages/Index.cshtml y Pages/Movies/Index.cshtml actualmente tienen el mismo título, pero
puede modificarlos para que tengan valores diferentes.
La propiedad Layout se establece en el archivo Pages/_ViewStart.cshtml:
@{
Layout = "_Layout";
}

El marcado anterior establece el archivo de diseño en Pages/_Layout.cshtml para todos los archivos de Razor en la
carpeta Pages. Vea Layout (Diseño) para más información.
Actualizar el diseño
Cambie el elemento <title> del archivo Pages/_Layout.cshtml para usar una cadena más corta.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - Movie</title>

Busque el siguiente elemento delimitador en el archivo Pages/_Layout.cshtml.

<a asp-page="/Index" class="navbar-brand">RazorPagesMovie</a>

Reemplace el elemento anterior por el marcado siguiente.

<a asp-page="/Movies/Index" class="navbar-brand">RpMovie</a>

El elemento delimitador anterior es una aplicación auxiliar de etiquetas. En este caso, se trata de la aplicación
auxiliar de etiquetas Anchor. El atributo y valor de la aplicación auxiliar de etiquetas asp-page="/Movies/Index"
crea un vínculo a la página de Razor /Movies/Index .
Guarde los cambios y pruebe la aplicación haciendo clic en el vínculo RpMovie. Consulte el archivo
_Layout.cshtml en GitHub.
Modelo de página Crear
Examine el modelo de página Pages/Movies/Create.cshtml.cs:
// Unused usings removed.
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesMovie.Models;
using System.Threading.Tasks;

namespace RazorPagesMovie.Pages.Movies
{
public class CreateModel : PageModel
{
private readonly RazorPagesMovie.Models.MovieContext _context;

public CreateModel(RazorPagesMovie.Models.MovieContext context)


{
_context = context;
}

public IActionResult OnGet()


{
return Page();
}

[BindProperty]
public Movie Movie { get; set; }

public async Task<IActionResult> OnPostAsync()


{
if (!ModelState.IsValid)
{
return Page();
}

_context.Movie.Add(Movie);
await _context.SaveChangesAsync();

return RedirectToPage("./Index");
}
}
}

El método OnGet inicializa cualquier estado necesario para la página. La página Crear no tiene ningún estado para
inicializar. El método Page crea un objeto PageResult que representa la página Create.cshtml.
La propiedad Movie usa el atributo [BindProperty] para participar en el enlace de modelos. Cuando el
formulario de creación publica los valores del formulario, el tiempo de ejecución de ASP.NET Core enlaza los
valores publicados con el modelo Movie .
El método OnPostAsync se ejecuta cuando la página publica los datos del formulario:

public async Task<IActionResult> OnPostAsync()


{
if (!ModelState.IsValid)
{
return Page();
}

_context.Movie.Add(Movie);
await _context.SaveChangesAsync();

return RedirectToPage("./Index");
}

Si hay algún error de modelo, se vuelve a mostrar el formulario, junto con los datos del formulario publicados. La
mayoría de los errores de modelo se pueden capturar en el cliente antes de que se publique el formulario. Un
ejemplo de un error de modelo consiste en publicar un valor para el campo de fecha que no se puede convertir en
una fecha. Más adelante en el tutorial volveremos a hablar de la validación del lado cliente y de la validación de
modelos.
Si no hay ningún error de modelo, los datos se guardan y el explorador se redirige a la página Índice.
Página de Razor Crear
Examine el archivo de la página de Razor Pages/Movies/Create.cshtml:

@page
@model RazorPagesMovie.Pages.Movies.CreateModel

@{
ViewData["Title"] = "Create";
}

<h2>Create</h2>

<h4>Movie</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Movie.Title" class="control-label"></label>
<input asp-for="Movie.Title" class="form-control" />
<span asp-validation-for="Movie.Title" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Movie.ReleaseDate" class="control-label"></label>
<input asp-for="Movie.ReleaseDate" class="form-control" />
<span asp-validation-for="Movie.ReleaseDate" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Movie.Genre" class="control-label"></label>
<input asp-for="Movie.Genre" class="form-control" />
<span asp-validation-for="Movie.Genre" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Movie.Price" class="control-label"></label>
<input asp-for="Movie.Price" class="form-control" />
<span asp-validation-for="Movie.Price" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</form>
</div>
</div>

<div>
<a asp-page="Index">Back to List</a>
</div>

@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

El elemento <form method="post"> es una aplicación auxiliar de etiquetas de formulario. La aplicación auxiliar de
etiquetas de formulario incluye automáticamente un token antifalsificación.
El motor de scaffolding crea un marcado de Razor para cada campo del modelo (excepto el identificador) similar
al siguiente:

<div asp-validation-summary="ModelOnly" class="text-danger"></div>


<div class="form-group">
<label asp-for="Movie.Title" class="control-label"></label>
<input asp-for="Movie.Title" class="form-control" />
<span asp-validation-for="Movie.Title" class="text-danger"></span>
</div>

Las aplicaciones auxiliares de etiquetas de validación ( <div asp-validation-summary y <span asp-validation-for )


muestran errores de validación. La validación se trata con más detalle en un punto posterior de esta serie.
La aplicación auxiliar de etiquetas ( <label asp-for="Movie.Title" class="control-label"></label> ) genera el título
de la etiqueta y el atributo for para la propiedad Title .
La aplicación auxiliar de etiquetas de entrada ( <input asp-for="Movie.Title" class="form-control" /> ) usa los
atributos DataAnnotations y genera los atributos HTML necesarios para la validación de jQuery en el lado del
cliente.
En el siguiente tutorial se describe SQLite y la propagación de la base de datos.

A N T E R IO R : A D IC IÓ N D E U N S IG U IE N T E :
M ODELO S Q L IT E
Trabajar con SQLite y páginas de Razor
25/06/2018 • 2 minutes to read • Edit Online

Por Rick Anderson


El objeto MovieContext controla la tarea de conexión a la base de datos y asignación de objetos Movie a los
registros de la base de datos. El contexto de base de datos se registra con el contenedor de inserción de
dependencias en el método ConfigureServices del archivo Startup.cs:

public void ConfigureServices(IServiceCollection services)


{
// requires
// using RazorPagesMovie.Models;
// using Microsoft.EntityFrameworkCore;

services.AddDbContext<MovieContext>(options =>
options.UseSqlite(Configuration.GetConnectionString("MovieContext")));
services.AddMvc();
}

SQLite
Según la información del sitio web de SQLite:

SQLite es un motor de base de datos SQL independiente, de alta confiabilidad, insertado, con características
completas y dominio público. SQLite es el motor de base de datos más usado en el mundo.

Existen muchas herramientas de terceros que se pueden descargar para administrar y ver una base de datos de
SQLite. La imagen de abajo es de DB Browser for SQLite. Si tiene una herramienta favorita de SQLite, deje un
comentario sobre lo que le gusta de ella.
Inicializar la base de datos
Cree una nueva clase denominada SeedData en la carpeta Models. Reemplace el código generado con el
siguiente:
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Linq;

namespace RazorPagesMovie.Models
{
public static class SeedData
{
public static void Initialize(IServiceProvider serviceProvider)
{
using (var context = new MovieContext(
serviceProvider.GetRequiredService<DbContextOptions<MovieContext>>()))
{
// Look for any movies.
if (context.Movie.Any())
{
return; // DB has been seeded
}

context.Movie.AddRange(
new Movie
{
Title = "When Harry Met Sally",
ReleaseDate = DateTime.Parse("1989-2-12"),
Genre = "Romantic Comedy",
Price = 7.99M
},

new Movie
{
Title = "Ghostbusters",
ReleaseDate = DateTime.Parse("1984-3-13"),
Genre = "Comedy",
Price = 8.99M
},

new Movie
{
Title = "Ghostbusters 2",
ReleaseDate = DateTime.Parse("1986-2-23"),
Genre = "Comedy",
Price = 9.99M
},

new Movie
{
Title = "Rio Bravo",
ReleaseDate = DateTime.Parse("1959-4-15"),
Genre = "Western",
Price = 3.99M
}
);
context.SaveChanges();
}
}
}
}

Si hay alguna película en la base de datos, se devuelve el inicializador.


if (context.Movie.Any())
{
return; // DB has been seeded.
}

Agregar el inicializador
Agregue el inicializador al método Main en el archivo Program.cs:

// Unused usings removed.


using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using RazorPagesMovie.Models;
using System;
using Microsoft.EntityFrameworkCore;

namespace RazorPagesMovie
{
public class Program
{
public static void Main(string[] args)
{
var host = BuildWebHost(args);

using (var scope = host.Services.CreateScope())


{
var services = scope.ServiceProvider;

try
{
var context = services.GetRequiredService<MovieContext>();
// requires using Microsoft.EntityFrameworkCore;
context.Database.Migrate();
// Requires using RazorPagesMovie.Models;
SeedData.Initialize(services);
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred seeding the DB.");
}
}

host.Run();
}

public static IWebHost BuildWebHost(string[] args) =>


WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.Build();
}
}

Prueba de la aplicación
Elimine todos los registros de la base de datos (para que se ejecute el método de inicialización). Detenga e inicie la
aplicación para inicializar la base de datos.
La aplicación muestra los datos inicializados.
A N T E R IO R : A D IC IÓ N D E U N S IG U IE N T E : A C T U A L IZ A C IÓ N D E
M ODELO P Á G IN A S
Actualización de las páginas generadas
25/06/2018 • 7 minutes to read • Edit Online

Por Rick Anderson


La aplicación de películas pinta bien, pero la presentación no es ideal. No queremos ver la hora (12:00:00 a.m. en
la imagen siguiente) y ReleaseDate debe ser Release Date (dos palabras).

Actualización del código generado


Abra el archivo Models/Movie.cs y agregue las líneas resaltadas mostradas en el código siguiente:

using System;
using System.ComponentModel.DataAnnotations;

namespace RazorPagesMovie.Models
{
public class Movie
{
public int ID { get; set; }
public string Title { get; set; }

[Display(Name = "Release Date")]


[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }
public string Genre { get; set; }
public decimal Price { get; set; }
}
}
En el próximo tutorial hablaremos de DataAnnotations. El atributo Display especifica qué se muestra como
nombre de un campo (en este caso, "Release Date" en lugar de "ReleaseDate"). El atributo DataType especifica el
tipo de los datos (Date), así que la información de hora almacenada en el campo no se muestra.
Vaya a Pages/Movies y mantenga el mouse sobre un vínculo de edición para ver la dirección URL de destino.

Los vínculos Edit, Details y Delete son generados por la aplicación auxiliar de etiquetas de delimitador del
archivo Pages/Movies/Index.cshtml.

@foreach (var item in Model.Movie) {


<tr>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.ReleaseDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Genre)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
<a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>

Las aplicaciones auxiliares de etiquetas permiten que el código de servidor participe en la creación y la
representación de elementos HTML en archivos de Razor. En el código anterior, AnchorTagHelper genera de forma
dinámica el valor del atributo href HTML desde la página de Razor (la ruta es relativa), el elemento asp-page y
el identificador de ruta ( asp-route-id ). Vea Generación de direcciones URL para las páginas para obtener más
información.
Use Ver código fuente en su explorador preferido para examinar el marcado generado. A continuación se
muestra una parte del HTML generado:

<td>
<a href="/Movies/Edit?id=1">Edit</a> |
<a href="/Movies/Details?id=1">Details</a> |
<a href="/Movies/Delete?id=1">Delete</a>
</td>

Los vínculos generados de forma dinámica pasan el identificador de la película con una cadena de consulta (por
ejemplo, http://localhost:5000/Movies/Details?id=2 ).
Actualice las páginas de edición, detalles y eliminación de Razor para usar la plantilla de ruta "{id:int}". Cambie la
directiva de página de cada una de estas páginas de @page a @page "{id:int}" . Ejecute la aplicación y luego vea
el origen. El HTML generado agrega el identificador a la parte de la ruta de acceso de la dirección URL:

<td>
<a href="/Movies/Edit/1">Edit</a> |
<a href="/Movies/Details/1">Details</a> |
<a href="/Movies/Delete/1">Delete</a>
</td>

Una solicitud a la página con la plantilla de ruta "{id:int}" que no incluya el entero devolverá un error HTTP 404
(no encontrado). Por ejemplo, http://localhost:5000/Movies/Details devolverá un error 404. Para que el
identificador sea opcional, anexe ? a la restricción de ruta:

@page "{id:int?}"

Actualización del control de excepciones de simultaneidad


Actualice el método OnPostAsync en el archivo Pages/Movies/Edit.cshtml.cs. En el código resaltado siguiente se
muestran los cambios:
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}

_context.Attach(Movie).State = EntityState.Modified;

try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!_context.Movie.Any(e => e.ID == Movie.ID))
{
return NotFound();
}
else
{
throw;
}
}

return RedirectToPage("./Index");
}

El código anterior solo detecta las excepciones de simultaneidad cuando el primer cliente simultáneo elimina la
película y el segundo cliente simultáneo publica cambios en ella.
Para probar el bloque catch :
Establecer un punto de interrupción en catch (DbUpdateConcurrencyException)
Edite una película.
En otra ventana del explorador, seleccione el vínculo de eliminación de la misma película y luego elimínela.
En la ventana anterior del explorador, publique los cambios en la película.
El código de producción por lo general debería detectar conflictos de simultaneidad cuando dos o más clientes
actualizan un registro de forma simultánea. Vea Administración de conflictos de simultaneidad para más
información.
Revisión de publicaciones y enlaces
Examine el archivo Pages/Movies/Edit.cshtml.cs: [!code-csharp]
Cuando se realiza una solicitud HTTP GET a la página Movies/Edit (por ejemplo,
http://localhost:5000/Movies/Edit/2 ):

El método OnGetAsync obtiene la película en la base de datos y devuelve el método Page .


El método Page presenta la página de Razor Pages/Movies/Edit.cshtml. El archivo Pages/Movies/Edit.cshtml
contiene la directiva de modelo ( @model RazorPagesMovie.Pages.Movies.EditModel ), que hace que el modelo de
película esté disponible en la página.
Se abre el formulario de edición con los valores de la película.
Cuando se publica la página Movies/Edit:
Los valores del formulario de la página se enlazan a la propiedad Movie . El atributo [BindProperty]
habilita el enlace de modelos.
[BindProperty]
public Movie Movie { get; set; }

Si hay errores en el estado del modelo (por ejemplo, ReleaseDate no se puede convertir en una fecha), el
formulario se vuelve a publicar con los valores enviados.
Si no hay ningún error en el modelo, se guarda la película.
Los métodos HTTP GET de las páginas de índice, creación y eliminación de Razor siguen un patrón similar. El
método HTTP POST OnPostAsync de la página de creación de Razor sigue un patrón similar al del método
OnPostAsync de la página de edición de Razor.

La búsqueda se incluye en el tutorial siguiente.

A N T E R IO R : T R A B A J A R C O N A GREGA R
S Q L IT E BÚSQUEDA
Adición de búsqueda a la aplicación páginas de
Razor
25/06/2018 • 6 minutes to read • Edit Online

Por Rick Anderson


En este documento se agrega a la página de índice una capacidad de búsqueda que permite buscar películas por
género o nombre.
Actualice el método OnGetAsync de la página de índice con el código siguiente:

@{
Layout = "_Layout";
}

public async Task OnGetAsync(string searchString)


{
var movies = from m in _context.Movie
select m;

if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}

Movie = await movies.ToListAsync();


}

La primera línea del método OnGetAsync crea una consulta LINQ para seleccionar las películas:

var movies = from m in _context.Movie


select m;

En este momento solo se define la consulta, no se ejecuta en la base de datos.


Si el parámetro searchString contiene una cadena, la consulta de películas se modifica para filtrar según la cadena
de búsqueda:

if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}

El código s => s.Title.Contains() es una expresión lambda. Las lambdas se usan en consultas LINQ basadas en
métodos como argumentos para métodos de operador de consulta estándar, como el método Where o Contains
(usado en el código anterior). Las consultas LINQ no se ejecutan cuando se definen ni cuando se modifican
mediante una llamada a un método (como Where , Contains u OrderBy ). En su lugar, se aplaza la ejecución de la
consulta. Esto significa que la evaluación de una expresión se aplaza hasta que su valor realizado se repite o se
llama al método ToListAsync . Para más información, vea Query Execution (Ejecución de consultas).
Nota: El método Contains se ejecuta en la base de datos, no en el código de C#. La distinción entre mayúsculas y
minúsculas en la consulta depende de la base de datos y la intercalación. En SQL Server, Contains se asigna a
SQL LIKE, que distingue entre mayúsculas y minúsculas. En SQLite, con la intercalación predeterminada, se
distingue entre mayúsculas y minúsculas.
Vaya a la página de películas y anexe una cadena de consulta como ?searchString=Ghost a la dirección URL (por
ejemplo, http://localhost:5000/Movies?searchString=Ghost ). Se muestran las películas filtradas.

Si se agrega la siguiente plantilla de ruta a la página de índice, la cadena de búsqueda se puede pasar como un
segmento de dirección URL (por ejemplo, http://localhost:5000/Movies/ghost ).

@page "{searchString?}"

La restricción de ruta anterior permite buscar el título como datos de ruta (un segmento de dirección URL ) en
lugar de como un valor de cadena de consulta. El elemento ? de "{searchString?}" significa que se trata de un
parámetro de ruta opcional.
Sin embargo, no se puede esperar que los usuarios modifiquen la dirección URL para buscar una película. En este
paso, se agrega la interfaz de usuario para filtrar las películas. Si ha agregado la restricción de ruta
"{searchString?}" , quítela.

Abra el archivo Pages/Movies/Index.cshtml y agregue el marcado <form> resaltado en el siguiente código:

@page
@model RazorPagesMovie.Pages.Movies.IndexModel

@{
ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
<a asp-page="Create">Create New</a>
</p>

<form>
<p>
Title: <input type="text" name="SearchString">
<input type="submit" value="Filter" />
</p>
</form>

<table class="table">
@*Markup removed for brevity.*@

La etiqueta HTML <form> usa la aplicación auxiliar de etiquetas de formulario. Cuando se envía el formulario, la
cadena de filtro se envía a la página Pages/Movies/Index. Guarde los cambios y pruebe el filtro.

Búsqueda por género


Agregue las siguientes propiedades resaltadas a Pages/Movies/Index.cshtml.cs:
public class IndexModel : PageModel
{
private readonly RazorPagesMovie.Models.MovieContext _context;

public IndexModel(RazorPagesMovie.Models.MovieContext context)


{
_context = context;
}

public IList<Movie> Movie { get; set; }


public SelectList Genres { get; set; }
public string MovieGenre { get; set; }

El elemento SelectList Genres contiene la lista de géneros. Esto permite al usuario seleccionar un género de la
lista.
La propiedad MovieGenre contiene el género concreto que selecciona el usuario (por ejemplo, "Western").
Actualice el método OnGetAsync con el código siguiente:

// Requires using Microsoft.AspNetCore.Mvc.Rendering;


public async Task OnGetAsync(string movieGenre, string searchString)
{
// Use LINQ to get list of genres.
IQueryable<string> genreQuery = from m in _context.Movie
orderby m.Genre
select m.Genre;

var movies = from m in _context.Movie


select m;

if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}

if (!String.IsNullOrEmpty(movieGenre))
{
movies = movies.Where(x => x.Genre == movieGenre);
}
Genres = new SelectList(await genreQuery.Distinct().ToListAsync());
Movie = await movies.ToListAsync();
}

El código siguiente es una consulta LINQ que recupera todos los géneros de la base de datos.

// Use LINQ to get list of genres.


IQueryable<string> genreQuery = from m in _context.Movie
orderby m.Genre
select m.Genre;

La SelectList de géneros se crea mediante la proyección de los distintos géneros.

Genres = new SelectList(await genreQuery.Distinct().ToListAsync());

Adición de búsqueda por género


Actualice Index.cshtml como se indica a continuación:
@page
@model RazorPagesMovie.Pages.Movies.IndexModel

@{
ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
<a asp-page="Create">Create New</a>
</p>

<form>
<p>
<select asp-for="MovieGenre" asp-items="Model.Genres">
<option value="">All</option>
</select>

Title: <input type="text" name="SearchString">


<input type="submit" value="Filter" />
</p>
</form>

<table class="table">
<thead>

Pruebe la aplicación al buscar por género, título de la película y ambos.

A N T E R IO R : A C T U A L IZ A C IÓ N D E S IG U IE N T E : A D IC IÓ N D E U N N U E V O
P Á G IN A S CAM PO
Creación de una aplicación web con ASP.NET Core
MVC en macOS con Visual Studio para Mac
21/06/2018 • 2 minutes to read • Edit Online

En esta serie de tutoriales aprenderá los aspectos básicos de la creación de una aplicación web de ASP.NET Core
MVC con Visual Studio para Mac.
En este tutorial se muestra el desarrollo web de ASP.NET Core MVC con controladores y vistas. Las páginas de
Razor son una nueva alternativa en ASP.NET Core 2.0 y posteriores, un modelo de programación basado en
páginas que facilita la compilación de interfaces de usuario web y hace que sean más productivas. Se recomienda
probar el tutorial de las páginas de Razor antes que la versión MVC. El tutorial de las páginas de Razor:
Es el método preferido para el desarrollo de nuevas aplicaciones.
Es más fácil de seguir.
Abarca más características.
Si elige este tutorial en lugar de la versión de páginas de Razor, le agradeceremos que nos explique el motivo en
este problema de GitHub.
1. Introducción
2. Agregar un controlador
3. Agregar una vista
4. Agregar un modelo
5. SQLite
6. Vistas y métodos de controlador
7. Agregar búsqueda
8. Agregar un campo nuevo
9. Agregar validación
10. Examinar los métodos Details y Delete
Introducción a ASP.NET Core MVC y Visual Studio
para Mac
25/06/2018 • 3 minutes to read • Edit Online

Por Rick Anderson


En este tutorial aprenderá los aspectos básicos de la creación de una aplicación web de ASP.NET Core MVC con
Visual Studio para Mac.
En este tutorial se muestra el desarrollo web de ASP.NET Core MVC con controladores y vistas. Las páginas de
Razor son una nueva alternativa en ASP.NET Core 2.0 y posteriores, un modelo de programación basado en
páginas que facilita la compilación de interfaces de usuario web y hace que sean más productivas. Se recomienda
probar el tutorial de las páginas de Razor antes que la versión MVC. El tutorial de las páginas de Razor:
Es el método preferido para el desarrollo de nuevas aplicaciones.
Es más fácil de seguir.
Abarca más características.
Si elige este tutorial en lugar de la versión de páginas de Razor, le agradeceremos que nos explique el motivo en
este problema de GitHub.
Hay tres versiones de este tutorial:
macOS: Creación de una aplicación de ASP.NET Core MVC con Visual Studio para Mac
Windows: Creación de una aplicación de ASP.NET Core MVC con Visual Studio
Linux, macOS y Windows: Creación de una aplicación de ASP.NET Core MVC con Visual Studio Code

Requisitos previos
Visual Studio for Mac

Creación de una aplicación web


Desde Visual Studio, seleccione Archivo > Nueva solución.
Seleccione Aplicación .NET Core > ASP.NET Core > Aplicación web > Siguiente.

Asigne el nombre MvcMovie al proyecto y, después, seleccione Crear.


Iniciar la aplicación
En Visual Studio, seleccione Ejecutar > Iniciar sin depurar para iniciar la aplicación. Visual Studio inicia Kestrel,
inicia un explorador y navega a http://localhost:port , donde port es un número de puerto elegido
aleatoriamente.
En la barra de direcciones aparece localhost:port# (y no algo como example.com ). Esto es así porque
localhost es el nombre de host estándar del equipo local. Cuando Visual Studio crea un proyecto web, se usa
un puerto aleatorio para el servidor web. Al ejecutar la aplicación verá otro puerto distinto.
Puede iniciar la aplicación en modo de depuración o en modo de no depuración desde el menú Ejecutar.
La plantilla predeterminada proporciona los vínculos Inicio, Acerca de y Contacto. En la imagen del explorador
anterior no se muestran estos vínculos. Según el tamaño del explorador, tendrá que hacer clic en el icono de
navegación para que se muestren.

En la siguiente sección de este tutorial conocerá MVC y empezará a escribir código.

S IG U IE N T E
Agregar un controlador a una aplicación de ASP.NET
Core MVC con Visual Studio para Mac
25/06/2018 • 10 minutes to read • Edit Online

Por Rick Anderson


El patrón de arquitectura del controlador de vista de modelos (MVC ) separa una aplicación en tres componentes
principales: Modelo, Vista y Controlador. El patrón de MVC ayuda a crear aplicaciones que son más fáciles de
actualizar y probar que las tradicionales aplicaciones monolíticas. Las aplicaciones basadas en MVC contienen:
Modelos: clases que representan los datos de la aplicación. Las clases de modelo usan lógica de validación
para aplicar las reglas de negocio para esos datos. Normalmente, los objetos de modelo recuperan y
almacenan el estado del modelo en una base de datos. En este tutorial, un modelo Movie recupera datos
de películas de una base de datos, los proporciona a la vista o los actualiza. Los datos actualizados se
escriben en una base de datos.
Vistas: las vistas son los componentes que muestran la interfaz de usuario de la aplicación. Por lo general,
esta interfaz de usuario muestra los datos del modelo.
Controladores: las clases que controlan las solicitudes del explorador. Recuperan los datos del modelo y
llaman a plantillas de vistas que devuelven una respuesta. En una aplicación MVC, la vista solo muestra
información; el controlador controla la interacción de los usuarios y los datos que introducen, y responde a
ellos. Por ejemplo, el controlador controla los datos de enrutamiento y los valores de cadena de consulta y
pasa estos valores al modelo. El modelo puede usar estos valores para consultar la base de datos. Por
ejemplo, http://localhost:1234/Home/About tiene datos de enrutamiento de Home (el controlador) y About
(el método de acción para llamar al controlador de inicio). http://localhost:1234/Movies/Edit/5 es una
solicitud para editar la película con ID=5 mediante el controlador de películas. Hablaremos sobre los datos
de enrutamiento más adelante en este tutorial.
El patrón de MVC ayuda a crear aplicaciones que separan los diferentes aspectos de la aplicación (lógica de
entrada, lógica comercial y lógica de la interfaz de usuario), a la vez que proporciona un acoplamiento vago entre
estos elementos. El patrón especifica dónde debe ubicarse cada tipo de lógica en la aplicación. La lógica de la
interfaz de usuario pertenece a la vista. La lógica de entrada pertenece al controlador. La lógica de negocios
pertenece al modelo. Esta separación ayuda a administrar la complejidad al compilar una aplicación, ya que
permite trabajar en uno de los aspectos de la implementación a la vez sin influir en el código de otro. Por ejemplo,
puede trabajar en el código de vista sin depender del código de lógica de negocios.
En esta serie de tutoriales se tratarán estos conceptos y se mostrará cómo usarlos para crear una aplicación de
película. El proyecto de MVC contiene carpetas para controladores y vistas.

Adición de un controlador
En el Explorador de soluciones, haga clic con el botón derecho en Controladores > Agregar > Nuevo
archivo.
Seleccione ASP.NET Core y Clase de controlador de MVC.
Asigne al controlador el nombre HelloWorldController.

Reemplace el contenido de Controllers/HelloWorldController.cs con lo siguiente:


using Microsoft.AspNetCore.Mvc;
using System.Text.Encodings.Web;

namespace MvcMovie.Controllers
{
public class HelloWorldController : Controller
{
//
// GET: /HelloWorld/

public string Index()


{
return "This is my default action...";
}

//
// GET: /HelloWorld/Welcome/

public string Welcome()


{
return "This is the Welcome action method...";
}
}
}

Cada método public en un controlador puede ser invocado como un punto de conexión HTTP. En el ejemplo
anterior, ambos métodos devuelven una cadena. Observe los comentarios delante de cada método.
Un extremo HTTP es una dirección URL que se puede usar como destino en la aplicación web, como por ejemplo
http://localhost:1234/HelloWorld . Combina el protocolo usado HTTP , la ubicación de red del servidor web
(incluido el puerto TCP ) localhost:1234 y el URI de destino HelloWorld .
El primer comentario dice que se trata de un método HTTP GET que se invoca mediante la anexión de
"/HelloWorld/" a la dirección URL base. El segundo comentario dice que se trata de un método HTTP GET que se
invoca mediante la anexión de "/HelloWorld/Welcome/" a la dirección URL. Más adelante en el tutorial usaremos
el motor de scaffolding para generar métodos HTTP POST .
Ejecute la aplicación en modo de no depuración y anexione "HelloWorld" a la ruta de acceso en la barra de
direcciones. El método Index devuelve una cadena.

MVC invoca las clases del controlador (y los métodos de acción que contienen) en función de la URL entrante. La
lógica de enrutamiento de URL predeterminada que usa MVC emplea un formato similar al siguiente para
determinar qué código se debe invocar:
/[Controller]/[ActionName]/[Parameters]

El formato para el enrutamiento se establece en el método Configure en Startup.cs.


app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});

Cuando se ejecuta la aplicación y no se suministra ningún segmento de dirección URL, de manera


predeterminada se usa el controlador "Home" y el método "Index" especificados en la línea de plantilla resaltada
arriba.
El primer segmento de dirección URL determina la clase de controlador que se va a ejecutar. De modo que
localhost:xxxx/HelloWorld se asigna a la clase HelloWorldController . La segunda parte del segmento de dirección
URL determina el método de acción en la clase. De modo que localhost:xxxx/HelloWorld/Index podría provocar
que se ejecute el método Index de la clase HelloWorldController . Tenga en cuenta que solo es necesario navegar
a localhost:xxxx/HelloWorld para que se llame al método Index de manera predeterminada. Esto es porque
Index es el método predeterminado al que se llamará en un controlador si no se especifica explícitamente un
nombre de método. La tercera parte del segmento de dirección URL ( id ) es para los datos de ruta. Veremos los
datos de ruta más adelante en este tutorial.
Vaya a http://localhost:xxxx/HelloWorld/Welcome . El método Welcome se ejecuta y devuelve la cadena "This is the
Welcome action method..." (Este es el método de acción de bienvenida). Para esta dirección URL, el controlador es
HelloWorld y Welcome es el método de acción. Todavía no ha usado el elemento [Parameters] de la dirección
URL.

Modifique el código para pasar cierta información del parámetro desde la dirección URL al controlador. Por
ejemplo: /HelloWorld/Welcome?name=Rick&numtimes=4 . Cambie el método Welcome para que incluya dos parámetros,
como se muestra en el código siguiente.

// GET: /HelloWorld/Welcome/
// Requires using System.Text.Encodings.Web;
public string Welcome(string name, int numTimes = 1)
{
return HtmlEncoder.Default.Encode($"Hello {name}, NumTimes is: {numTimes}");
}

El código anterior:
Usa la característica de parámetro opcional de C# para indicar que el parámetro numTimes tiene el valor
predeterminado 1 si no se pasa ningún valor para ese parámetro.
Usa HtmlEncoder.Default.Encode para proteger la aplicación de entradas malintencionadas (es decir,
JavaScript).
Usa cadenas interpoladas.
Ejecute la aplicación y navegue a:
http://localhost:xxxx/HelloWorld/Welcome?name=Rick&numtimes=4

(Reemplace xxxx con el número de puerto). Puede probar valores diferentes para name y numtimes en la dirección
URL. El sistema de enlace de modelos de MVC asigna automáticamente los parámetros con nombre de la cadena
de consulta en la barra de dirección a los parámetros del método. Vea Model Binding (Enlace de modelos) para
más información.

En la ilustración anterior, el segmento de dirección URL ( Parameters ) no se usa, y los parámetros name y
numTimes se pasan como cadenas de consulta. El ? (signo de interrogación) en la dirección URL anterior es un
separador y le siguen las cadenas de consulta. El carácter & separa las cadenas de consulta.
Reemplace el método Welcome con el código siguiente:

public string Welcome(string name, int ID = 1)


{
return HtmlEncoder.Default.Encode($"Hello {name}, ID: {ID}");
}

Ejecute la aplicación y escriba la dirección URL siguiente: http://localhost:xxx/HelloWorld/Welcome/3?name=Rick

Esta vez el tercer segmento de dirección URL coincide con el parámetro de ruta id . El método Welcome contiene
un parámetro id que coincide con la plantilla de dirección URL en el método MapRoute . El ? del final (en id? )
indica que el parámetro id es opcional.
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});

En estos ejemplos, el controlador ha realizado la parte "VC" de MVC, es decir, el trabajo de vista y de controlador.
El controlador devuelve HTML directamente. Por lo general, no es aconsejable que los controles devuelvan HTML
directamente, porque resulta muy complicado de programar y mantener. En su lugar, se suele usar un archivo de
plantilla de vista de Razor independiente para ayudar a generar la respuesta HTML. Haremos esto en el siguiente
tutorial.

A N T E R IO R S IG U IE N T E
Adición de una vista en una aplicación de ASP.NET
Core MVC
25/06/2018 • 14 minutes to read • Edit Online

Por Rick Anderson


En esta sección, se modificará la clase HelloWorldController para usar los archivos de plantilla de vista Razor con
el objetivo de encapsular correctamente el proceso de generar respuestas HTML a un cliente.
Para crear un archivo de plantilla de vista se usa Razor. Las plantillas de vista basadas en Razor tienen una
extensión de archivo .cshtml. Ofrecen una forma elegante de crear un resultado HTML mediante C#.
Actualmente, el método Index devuelve una cadena con un mensaje que está codificado de forma rígida en la
clase de controlador. En la clase HelloWorldController , reemplace el método Index por el siguiente código:

public IActionResult Index()


{
return View();
}

El código anterior devuelve un objeto View . Usa una plantilla de vista para generar una respuesta HTML al
explorador. Los métodos de controlador (también conocidos como métodos de acción), como el método Index
anterior, suelen devolver IActionResult o una clase derivada de ActionResult , en lugar de un tipo como una
cadena.

Agregar una vista


Haga clic con el botón derecho en la carpeta Vistas, haga clic en Agregar > Nueva carpeta y asigne a la
carpeta el nombre HelloWorld.
Haga clic con el botón derecho en la carpeta Views/HelloWorld y, luego, haga clic en Agregar > Nuevo
archivo.
En el cuadro de diálogo Nuevo archivo:
Seleccione Web en el panel izquierdo.
Seleccione Archivo HTML vacío en el panel central.
Escriba Index.cshtml en el cuadro Nombre.
Seleccione Nuevo.
Reemplace el contenido del archivo de vista de Razor Views/HelloWorld/Index.cshtml con lo siguiente:

@{
ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>Hello from our View Template!</p>

Navegue a http://localhost:xxxx/HelloWorld . El método Index en HelloWorldController no hizo mucho; ejecutó


la instrucción return View(); , que especificaba que el método debe usar un archivo de plantilla de vista para
representar una respuesta al explorador. Como no especificó expresamente el nombre del archivo de plantilla de
vista, MVC usó de manera predeterminada el archivo de vista Index.cshtml de la carpeta /Views/HelloWorld. La
imagen siguiente muestra la cadena "Hello from our View Template!" (Hola desde nuestra plantilla de vista)
codificada de forma rígida en la vista.

Si la ventana del explorador es pequeña (por ejemplo en un dispositivo móvil), es conveniente que alterne (pulse)
el botón de navegación de arranque en la esquina superior derecha para ver los vínculos Home (Inicio), About
(Acerca de) y Contact (Contacto).
Cambiar vistas y páginas de diseño
Pulse los vínculos de menú: MvcMovie (Película de MVC ), Home (Inicio), About (Acerca de). Cada página
muestra el mismo diseño de menú. El diseño de menú se implementa en el archivo Views/Shared/_Layout.cshtml.
Abra el archivo Views/Shared/_Layout.cshtml.
Las plantillas de diseño permiten especificar el diseño del contenedor HTML del sitio en un solo lugar y, después,
aplicarlo en varias páginas del sitio. Busque la línea @RenderBody() . RenderBody es un marcador de posición
donde se mostrarán todas las páginas específicas de vista que cree, encapsuladas en la página de diseño. Por
ejemplo, si selecciona el vínculo About (Acerca de), la vista Views/Home/About.cshtml se representa dentro
del método RenderBody .

Cambiar el título y el vínculo del menú en el archivo de diseño


En el elemento de título, cambie MvcMovie por Movie App . Cambie el texto del delimitador en la plantilla de
diseño de MvcMovie a Movie App y el controlador de Home a Movies como se resalta aquí:

[!code-html]
[!code-html]

WARNING
Aún no hemos implementado el controlador Movies , por lo que si hace clic en ese vínculo, obtendrá un error 404 (no
encontrado).

Guarde los cambios y pulse en el vínculo About (Acerca de). Observe cómo el título de la pestaña del explorador
muestra ahora About - Movie App (Acerca de - Aplicación de película) en lugar de About - Mvc Movie (Acerca
de - Aplicación de MVC ):
Pulse el vínculo Contacto y observe que el texto del título y el delimitador también muestran Movie App.
Hemos realizado el cambio una vez en la plantilla de diseño y hemos conseguido que todas las páginas del sitio
reflejen el nuevo texto de vínculo y el nuevo título.
Examine el archivo Views/_ViewStart.cshtml:

@{
Layout = "_Layout";
}

El archivo Views/_ViewStart.cshtml trae el archivo Views/Shared/_Layout.cshtml a cada vista. Puede usar la


propiedad Layout para establecer una vista de diseño diferente o establecerla en null para que no se use
ningún archivo de diseño.
Cambie el título de la vista Index .
Abra Views/HelloWorld/Index.cshtml. Los cambios se pueden hacer en dos lugares:
El texto que aparece en el título del explorador.
El encabezado secundario (elemento <h2> ).

Haremos que sean algo diferentes para que pueda ver qué parte del código cambia cada área de la aplicación.

@{
ViewData["Title"] = "Movie List";
}

<h2>My Movie List</h2>

<p>Hello from our View Template!</p>

En el código anterior, ViewData["Title"] = "Movie List"; establece la propiedad Title del diccionario ViewData
en "Movie List" (Lista de películas). La propiedad Title se usa en el elemento HTML <title> en la página de
diseño:

<title>@ViewData["Title"] - Movie App</title>

Guarde el cambio y navegue a http://localhost:xxxx/HelloWorld . Tenga en cuenta que el título del explorador, el
encabezado principal y los encabezados secundarios han cambiado. (Si no ve los cambios en el explorador, es
posible que esté viendo contenido almacenado en caché. Presione Ctrl+F5 en el explorador para forzar que se
cargue la respuesta del servidor). El título del explorador se crea con ViewData["Title"] , que se definió en la
plantilla de vista Index.cshtml y el texto "- Movie App" (-Aplicación de película) que se agregó en el archivo de
diseño.
Observe también cómo el contenido de la plantilla de vista Index.cshtml se fusionó con la plantilla de vista
Views/Shared/_Layout.cshtml y se envió una única respuesta HTML al explorador. Con las plantillas de diseño es
realmente fácil hacer cambios para que se apliquen en todas las páginas de la aplicación. Para saber más, vea
Layout (Diseño).

Nuestra pequeña cantidad de "datos", en este caso, el mensaje "Hello from our View Template!" (Hola desde
nuestra plantilla de vista), están codificados de forma rígida. La aplicación de MVC tiene una "V" (vista) y ha
obtenido una "C" (controlador), pero todavía no tiene una "M" (modelo).

Pasar datos del controlador a la vista


Las acciones del controlador se invocan en respuesta a una solicitud de dirección URL entrante. Una clase de
controlador es donde se escribe el código que controla las solicitudes entrantes del explorador. El controlador
recupera datos de un origen de datos y decide qué tipo de respuesta devolverá al explorador. Las plantillas de
vista se pueden usar desde un controlador para generar y dar formato a una respuesta HTML al explorador.
Los controladores se encargan de proporcionar los datos necesarios para que una plantilla de vista represente una
respuesta. Procedimiento recomendado: las plantillas de vista no deben realizar lógica de negocios ni interactuar
directamente con una base de datos. En su lugar, una plantilla de vista debe funcionar solo con los datos que le
proporciona el controlador. Mantener esta "separación de intereses" ayuda a mantener el código limpio, fácil de
probar y de mantener.
Actualmente, el método Welcome de la clase HelloWorldController toma un parámetro name y ID , y luego
obtiene los valores directamente en el explorador. En lugar de que el controlador represente esta respuesta como
una cadena, cambie el controlador para que use una plantilla de vista. La plantilla de vista genera una respuesta
dinámica, lo que significa que se deben pasar las partes de datos adecuadas desde el controlador a la vista para
que se genere la respuesta. Para hacerlo, indique al controlador que coloque los datos dinámicos (parámetros)
que necesita la plantilla de vista en un diccionario ViewData al que luego pueda obtener acceso la plantilla de
vista.
Vuelva al archivo HelloWorldController.cs y cambie el método Welcome para agregar un valor Message y
NumTimes al diccionario ViewData . El diccionario ViewData es un objeto dinámico, lo que significa que puede
colocar en él todo lo que quiera; el objeto ViewData no tiene ninguna propiedad definida hasta que coloca algo
dentro de él. El sistema de enlace de modelos de MVC asigna automáticamente los parámetros con nombre (
name y numTimes ) de la cadena de consulta en la barra de dirección a los parámetros del método. El archivo
HelloWorldController.cs completo tiene este aspecto:

using Microsoft.AspNetCore.Mvc;
using System.Text.Encodings.Web;

namespace MvcMovie.Controllers
{
public class HelloWorldController : Controller
{
public IActionResult Index()
{
return View();
}

public IActionResult Welcome(string name, int numTimes = 1)


{
ViewData["Message"] = "Hello " + name;
ViewData["NumTimes"] = numTimes;

return View();
}
}
}

El objeto de diccionario ViewData contiene datos que se pasarán a la vista.


Cree una plantilla de vista principal denominada Views/HelloWorld/Welcome.cshtml.
Se creará un bucle en la vista Welcome.cshtml que muestra "Hello" (Hola) NumTimes . Reemplace el contenido de
Views/HelloWorld/Welcome.cshtml con lo siguiente:

@{
ViewData["Title"] = "Welcome";
}

<h2>Welcome</h2>

<ul>
@for (int i = 0; i < (int)ViewData["NumTimes"]; i++)
{
<li>@ViewData["Message"]</li>
}
</ul>

Guarde los cambios y vaya a esta dirección URL:


http://localhost:xxxx/HelloWorld/Welcome?name=Rick&numtimes=4

Los datos se toman de la dirección URL y se pasan al controlador mediante el enlazador de modelos MVC. El
controlador empaqueta los datos en un diccionario ViewData y pasa ese objeto a la vista. Después, la vista
representa los datos como HTML en el explorador.
En el ejemplo anterior, usamos el diccionario ViewData para pasar datos del controlador a una vista. Más adelante
en el tutorial usaremos un modelo de vista para pasar datos de un controlador a una vista. El enfoque del modelo
de vista que consiste en pasar datos suele ser más preferible que el enfoque de diccionario ViewData . Para saber
más, vea ViewModel vs ViewData vs ViewBag vs TempData vs Session in MVC (ViewModel, ViewData, ViewBag,
TempData y Session en MVC ).
Bueno, todo esto era un tipo de "M" para el modelo, pero no el tipo de base de datos. Vamos a aprovechar lo que
hemos aprendido para crear una base de datos de películas.

A N T E R IO R S IG U IE N T E
Agregar un modelo a una aplicación ASP.NET Core
MVC con Visual Studio para Mac
25/06/2018 • 7 minutes to read • Edit Online

Adición de un modelo a una aplicación de ASP.NET Core


MVC
Por Rick Anderson y Tom Dykstra
En esta sección, agregará algunas clases para administrar películas en una base de datos. Estas clases serán el
elemento "Model" de la aplicación MVC.
Estas clases se usan con Entity Framework Core (EF Core) para trabajar con una base de datos. EF Core es un
marco de trabajo de asignación relacional de objetos (ORM ) que simplifica el código de acceso de datos que se
debe escribir. EF Core es compatible con muchos motores de base de datos.
Las clases de modelo que se crean se conocen como clases POCO (del inglés "plain-old CLR objects", objetos
CLR antiguos sin formato) porque no tienen ninguna dependencia de EF Core. Simplemente definen las
propiedades de los datos que se almacenan en la base de datos.
En este tutorial, se escriben primero las clases de modelo y EF Core creará la base de datos. Hay un enfoque
alternativo, que no se trata aquí, que consiste en generar clases de modelo a partir de una base de datos existente.
Para más información sobre este enfoque, vea ASP.NET Core - Existing Database (ASP.NET Core - base de datos
existente).

Agregar una clase de modelo de datos


Haga clic con el botón derecho en la carpeta Modelos y, luego, seleccione Agregar > Nuevo archivo.
En el cuadro de diálogo Nuevo archivo:
Seleccione General en el panel izquierdo.
Seleccione Clase vacía en el panel central.
Asigne a la clase el nombre Películas y seleccione Nuevo.
Agregue las propiedades siguientes a la clase Movie :

using System;

namespace MvcMovie.Models
{
public class Movie
{
public int ID { get; set; }
public string Title { get; set; }
public DateTime ReleaseDate { get; set; }
public string Genre { get; set; }
public decimal Price { get; set; }
}
}

La base de datos requiere el campo ID para la clave principal.


Compile el proyecto para comprobar que no contiene errores. Ahora tiene un modelo en su aplicación MVC.

Preparar el proyecto para la técnica scaffolding


Haga clic con el botón derecho en el archivo del proyecto y, luego, seleccione Herramientas > Editar
archivo.

Agregue los siguientes paquetes de NuGet resaltados al archivo MvcMovie.csproj:

<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.0.0" />
</ItemGroup>
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.0" />
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.0"
/>
</ItemGroup>
</Project>

Guarde el archivo.
Cree un archivo Modelos/MvcMovieContext.cs y agregue la siguiente clase MvcMovieContext : [!code-csharp]
Abra el archivo Startup.cs y agregue dos instrucciones using: [!code-csharp]
Agregue el contexto de base de datos al archivo Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddMvc();

services.AddDbContext<MvcMovieContext>(options =>
options.UseSqlite("Data Source=MvcMovie.db"));

Esto indica a Entity Framework las clases de modelo que se incluyen en el modelo de datos. Se va a definir
un conjunto de entidades de objetos Movie, que se representarán en la base de datos como una tabla de
películas.
Compile el proyecto para comprobar que no hay ningún error.

Aplicar la técnica scaffolding a MovieController


Abra una ventana de terminal en la carpeta del proyecto y ejecute los siguientes comandos:

dotnet restore
dotnet aspnet-codegenerator controller -name MoviesController -m Movie -dc MvcMovieContext --
relativeFolderPath Controllers --useDefaultLayout --referenceScriptLibraries

Si recibe el error No executable found matching command "dotnet-aspnet-codegenerator", verify :


Se encuentra en el directorio del proyecto. El directorio del proyecto tiene los archivos Program.cs, Startup.cs y
.csproj.
La versión de dotnet es la 1.1 o posterior. Ejecute dotnet para obtener la versión.
Ha agregado el elemento <DotNetCliToolReference> al archivo MvcMovie.csproj.

El motor de scaffolding crea lo siguiente:


Un controlador de películas (Controllers/MoviesController.cs)
Archivos de vistas de Razor para las páginas Crear, Eliminar, Detalles, Editar e Índice (Views/Movies/*.cshtml)
La creación automática de vistas y métodos de acción CRUD (crear, leer, actualizar y eliminar) se conoce como
scaffolding. Pronto contará con una aplicación web totalmente funcional que le permitirá administrar una base de
datos de películas.
Agregar los archivos a Visual Studio
Agregue el archivo MovieController.cs al proyecto de Visual Studio:
Haga clic con el botón derecho en la carpeta Controllers y seleccione Agregar > Agregar archivos.
Seleccione el archivo MovieController.cs.
Agregue las vistas y la carpeta Películas:
Haga clic con el botón derecho en la carpeta Vistas y seleccione Agregar > Agregar carpeta
existente.
Vaya a la carpeta Vistas, seleccione Vistas\Películas y seleccione Abrir.
En el cuadro de diálogo Seleccione los archivos que se deben agregar de Películas, seleccione
Incluir todo y Aceptar.

Realizar la migración inicial


Desde la línea de comandos, ejecute los comandos CLI de .NET Core:
dotnet ef migrations add InitialCreate
dotnet ef database update

El comando dotnet ef migrations add InitialCreate genera el código para crear el esquema de base de datos
inicial. El esquema se basa en el modelo especificado en DbContext (en el archivo Models/MvcMovieContext.cs). El
argumento Initial se usa para asignar nombre a las migraciones. Puede usar cualquier nombre, pero se suele
elegir uno que describa la migración. Vea Introduction to migrations (Introducción a las migraciones) para
obtener más información.
El comando dotnet ef database update ejecuta el método Up en el archivo Migrations/<time-
stamp>_InitialCreate.cs, con lo que se crea la base de datos.

Prueba de la aplicación
Ejecute la aplicación y pulse en el vínculo Mvc Movie (Película Mvc).
Pulse Create New (Crear nueva) y cree una película.

Es posible no que pueda escribir comas ni puntos decimales en el campo Price . Para admitir la validación
de jQuery para configuraciones regionales distintas del inglés que usan una coma (",") en lugar de un
punto decimal y formatos de fecha distintos de Estados Unidos, debe seguir unos pasos globalizar la
aplicación. Consulte https://github.com/aspnet/Docs/issues/4076 y recursos adicionales para obtener más
información. Por ahora, tan solo debe escribir números enteros como 10.
En algunas configuraciones regionales debe especificar el formato de fecha. Vea el código que aparece
resaltado a continuación.

using System;
using System.ComponentModel.DataAnnotations;

namespace MvcMovie.Models
{
public class Movie
{
public int ID { get; set; }
public string Title { get; set; }
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime ReleaseDate { get; set; }
public string Genre { get; set; }
public decimal Price { get; set; }
}
}

Hablaremos sobre DataAnnotations más adelante en el tutorial.


Al pulsar en Crear el formulario se envía al servidor, donde se guarda la información de la película en una base de
datos. La aplicación redirige a la URL /Movies, donde se muestra la información de la película que acaba de crear.

Cree un par de entradas más de película. Compruebe que todos los vínculos Editar, Detalles y Eliminar son
funcionales.
Ahora dispone de una base de datos y de páginas para mostrar, editar, actualizar y eliminar datos. En el siguiente
tutorial trabajaremos con la base de datos.

Recursos adicionales
Aplicaciones auxiliares de etiquetas
Globalización y localización
A N T E R IO R : A G R E G A R U N A S IG U IE N T E : T R A B A J A R C O N
V IS T A SQL
Trabajar con SQLite en un proyecto de ASP.NET Core
MVC
25/06/2018 • 2 minutes to read • Edit Online

Por Rick Anderson


El objeto MvcMovieContext controla la tarea de conexión a la base de datos y asignación de objetos Movie a los
registros de la base de datos. El contexto de base de datos se registra con el contenedor de inserción de
dependencias en el método ConfigureServices del archivo Startup.cs:

public void ConfigureServices(IServiceCollection services)


{
// Add framework services.
services.AddMvc();

services.AddDbContext<MvcMovieContext>(options =>
options.UseSqlite("Data Source=MvcMovie.db"));

SQLite
Según la información del sitio web de SQLite:

SQLite es un motor de base de datos SQL independiente, de alta confiabilidad, insertado, con características
completas y dominio público. SQLite es el motor de base de datos más usado en el mundo.

Existen muchas herramientas de terceros que se pueden descargar para administrar y ver una base de datos de
SQLite. La imagen de abajo es de DB Browser for SQLite. Si tiene una herramienta favorita de SQLite, deje un
comentario sobre lo que le gusta de ella.
Inicializar la base de datos
Cree una nueva clase denominada SeedData en la carpeta Models. Reemplace el código generado con el
siguiente:
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Linq;

namespace MvcMovie.Models
{
public static class SeedData
{
public static void Initialize(IServiceProvider serviceProvider)
{
using (var context = new MvcMovieContext(
serviceProvider.GetRequiredService<DbContextOptions<MvcMovieContext>>()))
{
// Look for any movies.
if (context.Movie.Any())
{
return; // DB has been seeded
}

context.Movie.AddRange(
new Movie
{
Title = "When Harry Met Sally",
ReleaseDate = DateTime.Parse("1989-1-11"),
Genre = "Romantic Comedy",
Price = 7.99M
},

new Movie
{
Title = "Ghostbusters ",
ReleaseDate = DateTime.Parse("1984-3-13"),
Genre = "Comedy",
Price = 8.99M
},

new Movie
{
Title = "Ghostbusters 2",
ReleaseDate = DateTime.Parse("1986-2-23"),
Genre = "Comedy",
Price = 9.99M
},

new Movie
{
Title = "Rio Bravo",
ReleaseDate = DateTime.Parse("1959-4-15"),
Genre = "Western",
Price = 3.99M
}
);
context.SaveChanges();
}
}
}
}

Si hay alguna película en la base de datos, se devuelve el inicializador.


if (context.Movie.Any())
{
return; // DB has been seeded.
}

Agregar el inicializador
Agregue el inicializador al método Main en el archivo Program.cs:
[!code-csharp]
[!code-csharp]
Prueba de la aplicación
Elimine todos los registros de la base de datos (para que se ejecute el método de inicialización). Detenga e inicie la
aplicación para inicializar la base de datos.
La aplicación muestra los datos inicializados.

A N T E R IO R : A G R E G A R U N S IG U IE N T E : V IS T A S Y M É T O D O S D E
M ODELO C ON TROL A D OR
Vistas y métodos de controlador en una aplicación
de ASP.NET Core MVC
25/06/2018 • 15 minutes to read • Edit Online

Por Rick Anderson


La aplicación de películas pinta bien, pero la presentación no es ideal. No queremos ver la hora (12:00:00 a.m. en
la imagen siguiente) y ReleaseDate deben ser dos palabras.

Abra el archivo Models/Movie.cs y agregue las líneas resaltadas que se muestran a continuación:

using System;
using System.ComponentModel.DataAnnotations;

namespace MvcMovie.Models
{
public class Movie
{
public int ID { get; set; }
public string Title { get; set; }

[Display(Name = "Release Date")]


[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }
public string Genre { get; set; }
public decimal Price { get; set; }
}
}

Compile y ejecute la aplicación.


En el próximo tutorial hablaremos de DataAnnotations. El atributo Display especifica qué se muestra como
nombre de un campo (en este caso, "Release Date" en lugar de "ReleaseDate"). El atributo DataType especifica el
tipo de los datos (Date), así que la información de hora almacenada en el campo no se muestra.
Vaya al controlador Movies y mantenga el puntero del mouse sobre un vínculo Edit (Editar) para ver la dirección
URL de destino.

Los vínculos Edit (Editar), Details (Detalles) y Delete (Eliminar) se generan mediante la aplicación auxiliar de
etiquetas de delimitador de MVC Core en el archivo Views/Movies/Index.cshtml.

<a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |


<a asp-action="Details" asp-route-id="@item.ID">Details</a> |
<a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>

Las aplicaciones auxiliares de etiquetas permiten que el código de servidor participe en la creación y la
representación de elementos HTML en archivos de Razor. En el código anterior, AnchorTagHelper genera
dinámicamente el valor del atributo HTML href a partir del identificador de ruta y el método de acción del
controlador. Use Ver código fuente en su explorador preferido o use las herramientas de desarrollo para
examinar el marcado generado. A continuación se muestra una parte del HTML generado:

<td>
<a href="/Movies/Edit/4"> Edit </a> |
<a href="/Movies/Details/4"> Details </a> |
<a href="/Movies/Delete/4"> Delete </a>
</td>

Recupere el formato para el enrutamiento establecido en el archivo Startup.cs:


app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});

ASP.NET Core traduce http://localhost:1234/Movies/Edit/4 en una solicitud al método de acción Edit del
controlador Movies con el parámetro Id de 4. (Los métodos de controlador también se denominan métodos de
acción).
Las aplicaciones auxiliares de etiquetas son una de las nuevas características más populares de ASP.NET Core.
Para más información, vea Recursos adicionales.
Abra el controlador Movies y examine los dos métodos de acción Edit . En el código siguiente se muestra el
método HTTP GET Edit , que captura la película y rellena el formulario de edición generado por el archivo de Razor
Edit.cshtml.

// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
return NotFound();
}

var movie = await _context.Movie.SingleOrDefaultAsync(m => m.ID == id);


if (movie == null)
{
return NotFound();
}
return View(movie);
}

En el código siguiente se muestra el método HTTP POST Edit , que procesa los valores de película publicados:
// POST: Movies/Edit/5
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("ID,Title,ReleaseDate,Genre,Price")] Movie movie)
{
if (id != movie.ID)
{
return NotFound();
}

if (ModelState.IsValid)
{
try
{
_context.Update(movie);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!MovieExists(movie.ID))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction("Index");
}
return View(movie);
}

El atributo [Bind] es una manera de proteger contra el exceso de publicación. Solo debe incluir propiedades en el
atributo [Bind] que quiera cambiar. Para más información, vea Protección del controlador frente al exceso de
publicación. ViewModels ofrece un enfoque alternativo para evitar el exceso de publicaciones.
Observe que el segundo método de acción Edit va precedido del atributo [HttpPost] .
// POST: Movies/Edit/5
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("ID,Title,ReleaseDate,Genre,Price")] Movie movie)
{
if (id != movie.ID)
{
return NotFound();
}

if (ModelState.IsValid)
{
try
{
_context.Update(movie);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!MovieExists(movie.ID))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction("Index");
}
return View(movie);
}

El atributo HttpPost especifica que este método Edit se puede invocar solamente para solicitudes POST . Podría
aplicar el atributo [HttpGet] al primer método de edición, pero no es necesario hacerlo porque [HttpGet] es el
valor predeterminado.
El atributo ValidateAntiForgeryToken se usa para impedir la falsificación de una solicitud y se empareja con un
token antifalsificación generado en el archivo de vista de edición (Views/Movies/Edit.cshtml). El archivo de vista de
edición genera el token antifalsificación con la aplicación auxiliar de etiquetas de formulario.

<form asp-action="Edit">

La aplicación auxiliar de etiquetas de formulario genera un token antifalsificación oculto que debe coincidir con el
token antifalsificación generado por [ValidateAntiForgeryToken] en el método Edit del controlador Movies. Para
más información, vea Prevención de ataques de falsificación de solicitudes.
El método HttpGet Edit toma el parámetro ID de la película, busca la película con el método
SingleOrDefaultAsync de Entity Framework y devuelve la película seleccionada a la vista de edición. Si no se
encuentra una película, se devuelve NotFound (HTTP 404).
// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
return NotFound();
}

var movie = await _context.Movie.SingleOrDefaultAsync(m => m.ID == id);


if (movie == null)
{
return NotFound();
}
return View(movie);
}

Cuando el sistema de scaffolding creó la vista de edición, examinó la clase Movie y creó código para representar
los elementos <label> y <input> para cada propiedad de la clase. En el ejemplo siguiente se muestra la vista de
edición que generó el sistema de scaffolding de Visual Studio:
@model MvcMovie.Models.Movie

@{
ViewData["Title"] = "Edit";
}

<h2>Edit</h2>

<form asp-action="Edit">
<div class="form-horizontal">
<h4>Movie</h4>
<hr />
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="ID" />
<div class="form-group">
<label asp-for="Title" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Title" class="form-control" />
<span asp-validation-for="Title" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="ReleaseDate" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="ReleaseDate" class="form-control" />
<span asp-validation-for="ReleaseDate" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="Genre" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Genre" class="form-control" />
<span asp-validation-for="Genre" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="Price" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Price" class="form-control" />
<span asp-validation-for="Price" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</div>
</div>
</form>

<div>
<a asp-action="Index">Back to List</a>
</div>

@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

Observe cómo la plantilla de vista tiene una instrucción @model MvcMovie.Models.Movie en la parte superior del
archivo. @model MvcMovie.Models.Movie especifica que la vista espera que el modelo de la plantilla de vista sea del
tipo Movie .
El código con scaffolding usa varios métodos de aplicación auxiliar de etiquetas para simplificar el marcado
HTML. La aplicación auxiliar de etiquetas muestra el nombre del campo: "Title" (Título), "ReleaseDate" (Fecha de
lanzamiento), "Genre" (Género) o "Price" (Precio). La aplicación auxiliar de etiquetas de entrada representa un
elemento HTML <input> . La aplicación auxiliar de etiquetas de validación muestra cualquier mensaje de
validación asociado a esa propiedad.
Ejecute la aplicación y navegue a la URL /Movies . Haga clic en un vínculo Edit (Editar). En el explorador, vea el
código fuente de la página. El código HTML generado para el elemento <form> se muestra abajo.

<form action="/Movies/Edit/7" method="post">


<div class="form-horizontal">
<h4>Movie</h4>
<hr />
<div class="text-danger" />
<input type="hidden" data-val="true" data-val-required="The ID field is required." id="ID" name="ID"
value="7" />
<div class="form-group">
<label class="control-label col-md-2" for="Genre" />
<div class="col-md-10">
<input class="form-control" type="text" id="Genre" name="Genre" value="Western" />
<span class="text-danger field-validation-valid" data-valmsg-for="Genre" data-valmsg-
replace="true"></span>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-2" for="Price" />
<div class="col-md-10">
<input class="form-control" type="text" data-val="true" data-val-number="The field Price must
be a number." data-val-required="The Price field is required." id="Price" name="Price" value="3.99" />
<span class="text-danger field-validation-valid" data-valmsg-for="Price" data-valmsg-
replace="true"></span>
</div>
</div>
<!-- Markup removed for brevity -->
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</div>
</div>
<input name="__RequestVerificationToken" type="hidden"
value="CfDJ8Inyxgp63fRFqUePGvuI5jGZsloJu1L7X9le1gy7NCIlSduCRx9jDQClrV9pOTTmqUyXnJBXhmrjcUVDJyDUMm7-
MF_9rK8aAZdRdlOri7FmKVkRe_2v5LIHGKFcTjPrWPYnc9AdSbomkiOSaTEg7RU" />
</form>

Los elementos <input> se muestran en un elemento HTML <form> cuyo atributo action se establece para
publicar en la dirección URL /Movies/Edit/id . Los datos del formulario se publicarán en el servidor cuando se
haga clic en el botón Save . La última línea antes del cierre del elemento </form> muestra el token XSRF oculto
generado por la aplicación auxiliar de etiquetas de formulario.

Procesamiento de la solicitud POST


En la siguiente lista se muestra la versión [HttpPost] del método de acción Edit .
// POST: Movies/Edit/5
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("ID,Title,ReleaseDate,Genre,Price")] Movie movie)
{
if (id != movie.ID)
{
return NotFound();
}

if (ModelState.IsValid)
{
try
{
_context.Update(movie);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!MovieExists(movie.ID))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction("Index");
}
return View(movie);
}

El atributo [ValidateAntiForgeryToken] valida el token XSRF oculto generado por el generador de tokens
antifalsificación en la herramienta auxiliar de etiquetas de formulario.
El sistema de enlace de modelos toma los valores de formulario publicados y crea un objeto Movie que se pasa
como el parámetro movie . El método ModelState.IsValid comprueba que los datos presentados en el formulario
pueden usarse para modificar (editar o actualizar) un objeto Movie . Si los datos son válidos, se guardan. Los
datos de película actualizados (o modificados) se guardan en la base de datos mediante una llamada al método
SaveChangesAsync del contexto de base de datos. Después de guardar los datos, el código redirige al usuario al
método de acción Index de la clase MoviesController , que muestra la colección de películas, incluidos los
cambios que se acaban de hacer.
Antes de que el formulario se envíe al servidor, la validación del lado cliente comprueba cualquier regla de
validación en los campos. Si hay errores de validación, se muestra un mensaje de error y no se publica el
formulario. Si JavaScript está deshabilitado, no dispondrá de la validación del lado cliente, sino que el servidor
detectará los valores publicados que no son válidos y los valores de formulario se volverán a mostrar con
mensajes de error. Más adelante en el tutorial se examina la validación de modelos con más detalle. La aplicación
auxiliar de etiquetas de validación en la plantilla de vista Views/Movies/Edit.cshtml se encarga de mostrar los
mensajes de error correspondientes.
Todos los métodos HttpGet del controlador de películas siguen un patrón similar. Obtienen un objeto de película
(o una lista de objetos, en el caso de Index ) y pasan el objeto (modelo) a la vista. El método Create pasa un
objeto de película vacío a la vista Create . Todos los métodos que crean, editan, eliminan o modifican los datos lo
hacen en la sobrecarga [HttpPost] del método. La modificación de datos en un método HTTP GET supone un
riesgo de seguridad. La modificación de datos en un método HTTP GET también infringe procedimientos
recomendados de HTTP y el patrón de arquitectura REST, que especifica que las solicitudes GET no deben
cambiar el estado de la aplicación. En otras palabras, realizar una operación GET debería ser una operación segura
sin efectos secundarios, que no modifica los datos persistentes.

Recursos adicionales
Globalización y localización
Introducción a las aplicaciones auxiliares de etiquetas
Creación de aplicaciones auxiliares de etiquetas
Prevención de ataques de falsificación de solicitudes
Protección del controlador frente al exceso de publicación
ViewModels
Aplicación auxiliar de etiquetas de formulario
Aplicación auxiliar de etiquetas de entrada
Aplicación auxiliar de etiquetas de elementos de etiqueta
Aplicación auxiliar de etiquetas de selección
Aplicación auxiliar de etiquetas de validación

A N T E R IO R : T R A B A J A R C O N S IG U IE N T E : A G R E G A R
S Q L IT E BÚSQUEDA
Adición de una búsqueda en una aplicación de
ASP.NET Core MVC
25/06/2018 • 12 minutes to read • Edit Online

Por Rick Anderson


En esta sección agregará capacidad de búsqueda para el método de acción Index que permite buscar películas
por género o nombre.
Actualice el método Index con el código siguiente:

public async Task<IActionResult> Index(string searchString)


{
var movies = from m in _context.Movie
select m;

if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}

return View(await movies.ToListAsync());


}

La primera línea del método de acción Index crea una consulta LINQ para seleccionar las películas:

var movies = from m in _context.Movie


select m;

En este momento solo se define la consulta, no se ejecuta en la base de datos.


Si el parámetro searchString contiene una cadena, la consulta de películas se modifica para filtrar según el valor
de la cadena de búsqueda:

if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}

El código s => s.Title.Contains() anterior es una expresión Lambda. Las lambdas se usan en consultas LINQ
basadas en métodos como argumentos para métodos de operador de consulta estándar, tales como el método
Where o Contains (usado en el código anterior). Las consultas LINQ no se ejecutan cuando se definen ni cuando
se modifican mediante una llamada a un método, como Where , Contains u OrderBy . En su lugar, se aplaza la
ejecución de la consulta. Esto significa que la evaluación de una expresión se aplaza hasta que su valor realizado se
repita realmente o se llame al método ToListAsync . Para más información sobre la ejecución de consultas en
diferido, vea Ejecución de la consulta.
Nota: El método Contains se ejecuta en la base de datos, no en el código de c# que se muestra arriba. La
distinción entre mayúsculas y minúsculas en la consulta depende de la base de datos y la intercalación. En SQL
Server, Contains se asigna a SQL LIKE, que distingue entre mayúsculas y minúsculas. En SQLite, con la
intercalación predeterminada, se distingue entre mayúsculas y minúsculas.
Navegue a /Movies/Index . Anexe una cadena de consulta como ?searchString=Ghost a la dirección URL. Se
muestran las películas filtradas.

Si se cambia la firma del método Index para que tenga un parámetro con el nombre id , el parámetro id
coincidirá con el marcador {id} opcional para el conjunto de rutas predeterminado en Startup.cs.

app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});

Nota: SQLlite distingue mayúsculas de minúsculas, por lo que tendrá que buscar "Ghost" y no "ghost".
El método Index anterior:

public async Task<IActionResult> Index(string searchString)


{
var movies = from m in _context.Movie
select m;

if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}

return View(await movies.ToListAsync());


}

El método Index actualizado con el parámetro id :


public async Task<IActionResult> Index(string id)
{
var movies = from m in _context.Movie
select m;

if (!String.IsNullOrEmpty(id))
{
movies = movies.Where(s => s.Title.Contains(id));
}

return View(await movies.ToListAsync());


}

Ahora puede pasar el título de la búsqueda como datos de ruta (un segmento de dirección URL ) en lugar de como
un valor de cadena de consulta.

Sin embargo, no se puede esperar que los usuarios modifiquen la dirección URL cada vez que quieran buscar una
película. Por tanto, ahora deberá agregar elementos de la interfaz de usuario con los que podrán filtrar las
películas. Si cambió la firma del método Index para probar cómo pasar el parámetro ID enlazado a una ruta,
vuelva a cambiarlo para que tome un parámetro denominado searchString :

public async Task<IActionResult> Index(string searchString)


{
var movies = from m in _context.Movie
select m;

if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}

return View(await movies.ToListAsync());


}

Abra el archivo Views/Movies/Index.cshtml y agregue el marcado <form> resaltado a continuación:


ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
<a asp-action="Create">Create New</a>
</p>

<form asp-controller="Movies" asp-action="Index">


<p>
Title: <input type="text" name="SearchString">
<input type="submit" value="Filter" />
</p>
</form>

<table class="table">
<thead>

La etiqueta HTML <form> usa la aplicación auxiliar de etiquetas de formulario, por lo que cuando se envía el
formulario, la cadena de filtro se registra en la acción Index del controlador de películas. Guarde los cambios y
después pruebe el filtro.

No hay ninguna sobrecarga [HttpPost] del método Index como cabría esperar. No es necesario, porque el
método no cambia el estado de la aplicación, simplemente filtra los datos.
Después, puede agregar el método [HttpPost] Index siguiente.

[HttpPost]
public string Index(string searchString, bool notUsed)
{
return "From [HttpPost]Index: filter on " + searchString;
}

El parámetro notUsed se usa para crear una sobrecarga para el método Index . Hablaremos sobre esto más
adelante en el tutorial.
Si agrega este método, el invocador de acción coincidiría con el método [HttpPost] Index , mientras que el
método [HttpPost] Index se ejecutaría tal como se muestra en la imagen de abajo.

Sin embargo, aunque agregue esta versión de [HttpPost] al método Index , hay una limitación en cómo se ha
implementado todo esto. Supongamos que quiere marcar una búsqueda en particular o que quiere enviar un
vínculo a sus amigos donde puedan hacer clic para ver la misma lista filtrada de películas. Tenga en cuenta que la
dirección URL de la solicitud HTTP POST es la misma que la dirección URL de la solicitud GET
(localhost:xxxxx/Movies/Index): no hay información de búsqueda en la URL. La información de la cadena de
búsqueda se envía al servidor como un valor de campo de formulario. Puede comprobarlo con las herramientas
de desarrollo del explorador o con la excelente herramienta Fiddler. En la imagen de abajo se muestran las
herramientas de desarrollo del explorador Chrome:
Puede ver el parámetro de búsqueda y el token XSRF en el cuerpo de la solicitud. Tenga en cuenta, como se
mencionó en el tutorial anterior, que la aplicación auxiliar de etiquetas de formulario genera un token XSRF
antifalsificación. Como no se van a modificar datos, no es necesario validar el token con el método del controlador.
El parámetro de búsqueda se encuentra en el cuerpo de solicitud y no en la dirección URL. Por eso no se puede
capturar dicha información para marcarla o compartirla con otros usuarios. Para solucionar este problema,
especificaremos que la solicitud sea HTTP GET .
Cambie la etiqueta <form> en la vista de Razor Views\movie\Index.cshtml para especificar method="get" :
<form asp-controller="Movies" asp-action="Index" method="get">

Ahora, cuando se envía una búsqueda, la URL contiene la cadena de consulta de búsqueda. La búsqueda también
será dirigida al método de acción HttpGet Index , aunque tenga un método HttpPost Index .

El marcado siguiente muestra el cambio en la etiqueta form :

<form asp-controller="Movies" asp-action="Index" method="get">

Agregar búsqueda por género


Agregue la clase MovieGenreViewModel siguiente a la carpeta Models:

using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace MvcMovie.Models
{
public class MovieGenreViewModel
{
public List<Movie> movies;
public SelectList genres;
public string movieGenre { get; set; }
}
}

El modelo de vista de película y género contendrá:


Una lista de películas.
SelectList , que contiene la lista de géneros. Esto permitirá al usuario seleccionar un género de la lista.
movieGenre , que contiene el género seleccionado.

Reemplace el método Index en MoviesController.cs por el código siguiente:


// Requires using Microsoft.AspNetCore.Mvc.Rendering;
public async Task<IActionResult> Index(string movieGenre, string searchString)
{
// Use LINQ to get list of genres.
IQueryable<string> genreQuery = from m in _context.Movie
orderby m.Genre
select m.Genre;

var movies = from m in _context.Movie


select m;

if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}

if (!String.IsNullOrEmpty(movieGenre))
{
movies = movies.Where(x => x.Genre == movieGenre);
}

var movieGenreVM = new MovieGenreViewModel();


movieGenreVM.genres = new SelectList(await genreQuery.Distinct().ToListAsync());
movieGenreVM.movies = await movies.ToListAsync();

return View(movieGenreVM);
}

El código siguiente es una consulta LINQ que recupera todos los géneros de la base de datos.

// Use LINQ to get list of genres.


IQueryable<string> genreQuery = from m in _context.Movie
orderby m.Genre
select m.Genre;

La SelectList de géneros se crea mediante la proyección de los distintos géneros (no queremos que nuestra lista
de selección tenga géneros duplicados).

movieGenreVM.genres = new SelectList(await genreQuery.Distinct().ToListAsync())

Agregar búsqueda por género a la vista de índice


Actualice Index.cshtml de la siguiente manera:
@model MvcMovie.Models.MovieGenreViewModel

@{
ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
<a asp-action="Create">Create New</a>
</p>

<form asp-controller="Movies" asp-action="Index" method="get">


<p>
<select asp-for="movieGenre" asp-items="Model.genres">
<option value="">All</option>
</select>

Title: <input type="text" name="SearchString">


<input type="submit" value="Filter" />
</p>
</form>

<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.movies[0].Title)
</th>
<th>
@Html.DisplayNameFor(model => model.movies[0].ReleaseDate)
</th>
<th>
@Html.DisplayNameFor(model => model.movies[0].Genre)
</th>
<th>
@Html.DisplayNameFor(model => model.movies[0].Price)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.movies)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.ReleaseDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Genre)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
<a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-action="Details" asp-route-id="@item.ID">Details</a> |
<a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
Examine la expresión lambda usada en la siguiente aplicación auxiliar HTML:
@Html.DisplayNameFor(model => model.movies[0].Title)

En el código anterior, la aplicación auxiliar HTML DisplayNameFor inspecciona la propiedad Title a la que se
hace referencia en la expresión lambda para determinar el nombre para mostrar. Puesto que la expresión lambda
se inspecciona en lugar de evaluarse, no recibirá una infracción de acceso cuando model , model.movies o
model.movies[0] sean null o estén vacíos. Cuando se evalúa la expresión lambda (por ejemplo,
@Html.DisplayFor(modelItem => item.Title) ), se evalúan los valores de propiedad del modelo.

Pruebe la aplicación haciendo búsquedas por género, título de la película y ambos.

A N T E R IO R : V IS T A S Y M É T O D O S D E S IG U IE N T E : A G R E G A R U N
C ON TROL A D OR CAM PO
Adición de un nuevo campo
25/06/2018 • 5 minutes to read • Edit Online

Por Rick Anderson


En este tutorial se agregará un nuevo campo a la tabla Movies . Quitaremos la base de datos y crearemos una
nueva cuando cambiemos el esquema (agregar un nuevo campo). Este flujo de trabajo funciona bien al principio
de desarrollo si no tenemos que conservar datos de producción.
Una vez que se haya implementado la aplicación y se tengan datos que se quieran conservar, no se podrá
desconectar la base de datos cuando sea necesario cambiar el esquema. Migraciones de Entity Framework Code
First permite actualizar el esquema y migrar la base de datos sin perder datos. Migraciones es una característica
muy usada con SQL Server, pero SQLite no admite muchas operaciones de esquema de migración, por lo que
solo se pueden realizar migraciones muy sencillas. Para más información, vea SQLite EF Core Database Provider
Limitations (Limitaciones del proveedor de base de datos de SQLite EF Core).

Adición de una propiedad de clasificación al modelo Movie


Abra el archivo Models/Movie.cs y agregue una propiedad Rating :
[!code-csharp]
[!code-csharp]
Dado que ha agregado un nuevo campo a la clase Movie , también debe actualizar la lista blanca de direcciones de
enlace para que incluya esta nueva propiedad. En MoviesController.cs, actualice el atributo [Bind] para que los
métodos de acción Create y Edit incluyan la propiedad Rating :

[Bind("ID,Title,ReleaseDate,Genre,Price,Rating")]

También necesita actualizar las plantillas de vista para mostrar, crear y editar la nueva propiedad Rating en la
vista del explorador.
Edite el archivo /Views/Movies/Index.cshtml y agregue un campo Rating :
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.movies[0].Title)
</th>
<th>
@Html.DisplayNameFor(model => model.movies[0].ReleaseDate)
</th>
<th>
@Html.DisplayNameFor(model => model.movies[0].Genre)
</th>
<th>
@Html.DisplayNameFor(model => model.movies[0].Price)
</th>
<th>
@Html.DisplayNameFor(model => model.movies[0].Rating)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.movies)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.ReleaseDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Genre)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
@Html.DisplayFor(modelItem => item.Rating)
</td>
<td>

Actualice /Views/Movies/Create.cshtml con un campo Rating .


La aplicación no funcionará hasta que la base de datos se actualice para incluir el nuevo campo. Si la ejecuta
ahora, se producirá la siguiente SqliteException :

SqliteException: SQLite Error 1: 'no such column: m.Rating'.

Este error se muestra porque la clase del modelo Movie actualizada es diferente del esquema de la tabla Movie de
la base de datos existente. (No hay ninguna columna Rating en la tabla de la base de datos).
Este error se puede resolver de varias maneras:
1. Desconecte la base de datos y haga que Entity Framework vuelva a crear automáticamente la base de datos
según el nuevo esquema de clase de modelo. Con este enfoque se pierden los datos que tenga en la base
de datos, así que no puede hacer esto con una base de datos de producción. A menudo, una forma
productiva de desarrollar una aplicación consiste en usar un inicializador para propagar una base de datos
con datos de prueba.
2. Modifique manualmente el esquema de la base de datos existente para que coincida con las clases de
modelo. La ventaja de este enfoque es que se conservan los datos. Puede realizar este cambio de forma
manual o mediante la creación de un script de cambio de base de datos.
3. Use Migraciones de Code First para actualizar el esquema de la base de datos.
Para este tutorial, se desconectará la base de datos y se volverá a crear cuando cambie el esquema. Para
desconectar la base de datos, ejecute este comando desde un terminal:
dotnet ef database drop

Actualice la clase SeedData para que proporcione un valor para la nueva columna. A continuación se muestra un
cambio de ejemplo, aunque es conveniente realizarlo con cada new Movie .

new Movie
{
Title = "When Harry Met Sally",
ReleaseDate = DateTime.Parse("1989-1-11"),
Genre = "Romantic Comedy",
Rating = "R",
Price = 7.99M
},

Agregue el campo Rating a la vista Edit , Details y Delete .


Ejecute la aplicación y compruebe que puede crear, editar o mostrar películas con un campo Rating . plantillas.

A N T E R IO R : A G R E G A R S IG U IE N T E : A G R E G A R
BÚSQUEDA V A L ID A C IÓ N
Adición de validación
25/06/2018 • 17 minutes to read • Edit Online

Por Rick Anderson


En esta sección se agregará lógica de validación al modelo Movie y se asegurará de que las reglas de validación
se aplican siempre que un usuario crea o edita una película.

Respetar el principio DRY


Uno de los principios de diseño de MVC es DRY ("Una vez y solo una"). ASP.NET MVC le anima a que especifique
la función o el comportamiento una sola vez y luego lo refleje en el resto de la aplicación. Esto reduce la cantidad
de código que necesita escribir y hace que el código que escribe sea menos propenso a errores, así como más fácil
probar y de mantener.
La compatibilidad de validación proporcionada por MVC y Entity Framework Core Code First es un buen ejemplo
del principio DRY. Puede especificar las reglas de validación mediante declaración en un lugar (en la clase del
modelo) y las reglas se aplican en toda la aplicación.

Adición de reglas de validación al modelo de película


Abra el archivo Movie.cs. DataAnnotations proporciona un conjunto integrado de atributos de validación que se
aplican mediante declaración a cualquier clase o propiedad. (También contiene atributos de formato como
DataType , que ayudan a aplicar formato y no proporcionan validación).

Actualice la clase Movie para aprovechar los atributos de validación integrados Required , StringLength ,
RegularExpression y Range .
[!code-csharp]
[!code-csharp]
Los atributos de validación especifican el comportamiento que quiere aplicar en las propiedades del modelo al
que se aplican. Los atributos Required y MinimumLength indican que una propiedad debe tener un valor, pero nada
evita que un usuario escriba espacios en blanco para satisfacer esta validación. El atributo RegularExpression se
usa para limitar los caracteres que se pueden escribir. En el código anterior, Genre y Rating solamente pueden
usar letras (no se permiten espacios en blanco, números ni caracteres especiales). El atributo Range restringe un
valor a un intervalo determinado. El atributo StringLength permite establecer la longitud máxima de una
propiedad de cadena y, opcionalmente, su longitud mínima. Los tipos de valor (como decimal , int , float ,
DateTime ) son intrínsecamente necesarios y no necesitan el atributo [Required] .

Cuando ASP.NET aplica automáticamente reglas de validación, logramos que la aplicación sea más sólida.
También nos permite asegurarnos de que todo se valida y que no nos dejamos ningún dato incorrecto en la base
de datos accidentalmente.

Interfaz de usuario de error de validación en MVC


Ejecute la aplicación y navegue al controlador Movies.
Pulse el vínculo Crear nueva para agregar una nueva película. Rellene el formulario con algunos valores no
válidos. En cuanto la validación del lado cliente de jQuery detecta el problema, muestra un mensaje de error.
NOTE
Es posible que no pueda escribir comas decimales en el campo Price . Para que la validación de jQuery sea compatible con
configuraciones regionales distintas del inglés que usan una coma (",") en lugar de un punto decimal y formatos de fecha
distintos del de Estados Unidos, debe seguir unos pasos para globalizar la aplicación. Consulte el problema 4076 de GitHub
para obtener instrucciones sobre cómo agregar la coma decimal.

Observe cómo el formulario presenta automáticamente un mensaje de error de validación adecuado en cada
campo que contiene un valor no válido. Los errores se aplican en el lado cliente (con JavaScript y jQuery) y en el
lado servidor (cuando un usuario tiene JavaScript deshabilitado).
Una ventaja importante es que no fue necesario cambiar ni una sola línea de código en la clase MoviesController
o en la vista Create.cshtml para habilitar esta interfaz de usuario de validación. El controlador y las vistas que creó
en pasos anteriores de este tutorial seleccionaron automáticamente las reglas de validación que especificó
mediante atributos de validación en las propiedades de la clase del modelo Movie . Pruebe la aplicación mediante
el método de acción Edit y se aplicará la misma validación.
Los datos del formulario no se enviarán al servidor hasta que dejen de producirse errores de validación de cliente.
Puede comprobarlo colocando un punto de interrupción en el método HTTP Post mediante la herramienta Fiddler
o las herramientas de desarrollo F12.

Cómo funciona la validación


Tal vez se pregunte cómo se generó la validación de la interfaz de usuario sin actualizar el código en el controlador
o las vistas. En el código siguiente se muestran los dos métodos Create .

// GET: Movies/Create
public IActionResult Create()
{
return View();
}

// POST: Movies/Create
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(
[Bind("ID,Title,ReleaseDate,Genre,Price, Rating")] Movie movie)
{
if (ModelState.IsValid)
{
_context.Add(movie);
await _context.SaveChangesAsync();
return RedirectToAction("Index");
}
return View(movie);
}

El primer método de acción Create (HTTP GET) muestra el formulario de creación inicial. La segunda versión (
[HttpPost] ) controla el envío de formulario. El segundo método Create (la versión [HttpPost] ) llama a
ModelState.IsValid para comprobar si la película tiene errores de validación. Al llamar a este método se evalúan
todos los atributos de validación que se hayan aplicado al objeto. Si el objeto tiene errores de validación, el
método Create vuelve a mostrar el formulario. Si no hay ningún error, el método guarda la nueva película en la
base de datos. En nuestro ejemplo de película, el formulario no se publica en el servidor si se detectan errores de
validación del lado cliente; cuando hay errores de validación en el lado cliente, no se llama nunca al segundo
método Create . Si deshabilita JavaScript en el explorador, se deshabilita también la validación del cliente y puede
probar si el método Create HTTP POST ModelState.IsValid detecta errores de validación.
Puede establecer un punto de interrupción en el método [HttpPost] Create y comprobar si nunca se llama al
método. La validación del lado cliente no enviará los datos del formulario si se detectan errores de validación. Si
deshabilita JavaScript en el explorador y después envía el formulario con errores, se alcanzará el punto de
interrupción. Puede seguir obteniendo validación completa sin JavaScript.
En la siguiente imagen se muestra cómo deshabilitar JavaScript en el explorador Firefox.
En la siguiente imagen se muestra cómo deshabilitar JavaScript en el explorador Chrome.

Después de deshabilitar JavaScript, publique los datos no válidos y siga los pasos del depurador.
Abajo se muestra una parte de la plantilla de vista Create.cshtml a la que se aplicó scaffolding en un paso anterior
de este tutorial. Los métodos de acción que se muestran arriba la usan para mostrar el formulario inicial y para
volver a mostrarlo en caso de error.

<form asp-action="Create">
<div class="form-horizontal">
<h4>Movie</h4>
<hr />

<div asp-validation-summary="ModelOnly" class="text-danger"></div>


<div class="form-group">
<label asp-for="Title" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Title" class="form-control" />
<span asp-validation-for="Title" class="text-danger"></span>
</div>
</div>

@*Markup removed for brevity.*@


</div>
</form>

La aplicación auxiliar de etiquetas de entrada usa los atributos DataAnnotations y genera los atributos HTML
necesarios para la validación de jQuery en el lado cliente. La aplicación auxiliar de etiquetas de validación muestra
errores de validación. Para más información, vea Introduction to model validation in ASP.NET Core MVC
(Introducción a la validación de modelos en ASP.NET Core MVC ).
Lo realmente bueno de este enfoque es que ni el controlador ni la plantilla de vista Create saben que las reglas
de validación actuales se están aplicando ni conocen los mensajes de error específicos que se muestran. Las reglas
de validación y las cadenas de error solo se especifican en la clase Movie . Estas mismas reglas de validación se
aplican automáticamente a la vista Edit y a cualquier otra vista de plantillas creada que edite el modelo.
Cuando necesite cambiar la lógica de validación, puede hacerlo exactamente en un solo lugar mediante la adición
de atributos de validación al modelo (en este ejemplo, la clase Movie ). No tendrá que preocuparse de que
diferentes partes de la aplicación sean incoherentes con el modo en que se aplican las reglas: toda la lógica de
validación se definirá en un solo lugar y se usará en todas partes. Esto mantiene el código muy limpio y hace que
sea fácil de mantener y evolucionar. También significa que respeta totalmente el principio DRY.

Uso de atributos DataType


Abra el archivo Movie.cs y examine la clase Movie . El espacio de nombres System.ComponentModel.DataAnnotations
proporciona atributos de formato además del conjunto integrado de atributos de validación. Ya hemos aplicado un
valor de enumeración DataType en la fecha de lanzamiento y los campos de precio. En el código siguiente se
muestran las propiedades ReleaseDate y Price con el atributo DataType adecuado.

[Display(Name = "Release Date")]


[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }

[Range(1, 100)]
[DataType(DataType.Currency)]
public decimal Price { get; set; }

Los atributos DataType solo proporcionan sugerencias para que el motor de vista aplique formato a los datos (y
ofrece atributos o elementos como <a> para las direcciones URL y <a href="mailto:EmailAddress.com"> para el
correo electrónico). Use el atributo RegularExpression para validar el formato de los datos. El atributo DataType
no es un atributo de validación, sino que se usa para especificar un tipo de datos más específico que el tipo
intrínseco de la base de datos. En este caso solo queremos realizar un seguimiento de la fecha, no la hora. La
enumeración DataType proporciona muchos tipos de datos, como Date (Fecha), Time (Hora), PhoneNumber
(Número de teléfono), Currency (Moneda), EmailAddress (Dirección de correo electrónico), etc. El atributo
DataType también puede permitir que la aplicación proporcione automáticamente características específicas del
tipo. Por ejemplo, se puede crear un vínculo mailto: para DataType.EmailAddress y se puede proporcionar un
selector de datos para DataType.Date en exploradores compatibles con HTML5. Los atributos DataType emiten
atributos HTML 5 data- (se pronuncia "datos dash") que los exploradores HTML 5 pueden comprender. Los
atributos DataType no proporcionan ninguna validación.
DataType.Date no especifica el formato de la fecha que se muestra. De manera predeterminada, el campo de
datos se muestra según los formatos predeterminados basados en el elemento CultureInfo del servidor.
El atributo DisplayFormat se usa para especificar el formato de fecha de forma explícita:

[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]


public DateTime ReleaseDate { get; set; }

El valor ApplyFormatInEditMode especifica que el formato se debe aplicar también cuando el valor se muestra en
un cuadro de texto para su edición. En algunos campos este comportamiento puede no ser conveniente. Por poner
un ejemplo, es probable que con valores de moneda no se quiera que el símbolo de la divisa se incluya en el
cuadro de texto editable.
El atributo DisplayFormat puede usarse por sí solo, pero normalmente se recomienda usar el atributo DataType .
El atributo DataType transmite la semántica de los datos en contraposición a cómo se representa en una pantalla
y ofrece las siguientes ventajas que no proporciona DisplayFormat:
El explorador puede habilitar características de HTML5 (por ejemplo, mostrar un control de calendario, el
símbolo de moneda adecuado según la configuración regional, vínculos de correo electrónico, etc.).
De manera predeterminada, el explorador representa los datos con el formato correcto según la
configuración regional.
El atributo DataType puede habilitar MVC para que elija la plantilla de campo adecuada para representar
los datos ( DisplayFormat , si se usa por sí solo, usa la plantilla de cadena).

NOTE
La validación de jQuery no funciona con el atributo Range ni DateTime . Por ejemplo, el código siguiente siempre muestra
un error de validación del lado cliente, incluso cuando la fecha está en el intervalo especificado:
[Range(typeof(DateTime), "1/1/1966", "1/1/2020")]

Debe deshabilitar la validación de fechas de jQuery para usar el atributo Range con DateTime . Por lo general no
se recomienda compilar fechas fijas en los modelos, así que desaconseja usar el atributo Range y DateTime .
El código siguiente muestra la combinación de atributos en una línea:
[!code-csharp]
[!code-csharp]
En la siguiente parte de la serie de tutoriales, revisaremos la aplicación y realizaremos algunas mejoras a los
métodos Details y Delete generados automáticamente.

Recursos adicionales
Trabajar con formularios
Globalización y localización
Introducción a las aplicaciones auxiliares de etiquetas
Creación de aplicaciones auxiliares de etiquetas

A N T E R IO R : A G R E G A R U N S IG U IE N T E : E X A M IN A R L O S M É T O D O S D E T A IL S Y
CAM PO D E L E TE
Examinar los métodos Details y Delete de una
aplicación ASP.NET Core
25/06/2018 • 5 minutes to read • Edit Online

Por Rick Anderson


Abra el controlador Movie y examine el método Details :
[!code-csharp]
[!code-csharp]
El motor de scaffolding de MVC que creó este método de acción agrega un comentario en el que se muestra una
solicitud HTTP que invoca el método. En este caso se trata de una solicitud GET con tres segmentos de dirección
URL, el controlador Movies , el método Details y un valor id . Recuerde que estos segmentos se definen en
Startup.cs.

app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});

EF facilita el proceso de búsqueda de datos mediante el método SingleOrDefaultAsync . Una característica de


seguridad importante integrada en el método es que el código comprueba que el método de búsqueda haya
encontrado una película antes de intentar hacer nada con ella. Por ejemplo, un pirata informático podría introducir
errores en el sitio cambiando la dirección URL creada por los vínculos de http://localhost:xxxx/Movies/Details/1 a
algo parecido a http://localhost:xxxx/Movies/Details/12345 (o algún otro valor que no represente una película
real). Si no comprobara una película null, la aplicación generaría una excepción.
Examine los métodos Delete y DeleteConfirmed .
[!code-csharp]
[!code-csharp]
Tenga en cuenta que el método HTTP GET Delete no elimina la película especificada, sino que devuelve una vista de
la película donde puede enviar (HttpPost) la eliminación. La acción de efectuar una operación de eliminación en
respuesta a una solicitud GET (o con este propósito efectuar una operación de edición, creación o cualquier otra
operación que modifique los datos) genera una vulnerabilidad de seguridad.
El método [HttpPost] que elimina los datos se denomina DeleteConfirmed para proporcionar al método HTTP
POST una firma o nombre únicos. Las dos firmas de método se muestran a continuación:

// GET: Movies/Delete/5
public async Task<IActionResult> Delete(int? id)
{
// POST: Movies/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{

Common Language Runtime (CLR ) requiere métodos sobrecargados para disponer de una firma de parámetro
única (mismo nombre de método, pero lista de parámetros diferente). En cambio, aquí necesita dos métodos
Delete (uno para GET y otro para POST ) que tienen la misma firma de parámetro (ambos deben aceptar un
número entero como parámetro).
Hay dos enfoques para este problema. Uno consiste en proporcionar nombres diferentes a los métodos, que es lo
que hizo el mecanismo de scaffolding en el ejemplo anterior. Pero esto implica un pequeño problema: ASP.NET
asigna segmentos de una dirección URL a los métodos de acción por nombre y, si cambia el nombre de un método,
normalmente el enrutamiento no podría encontrar ese método. La solución es la que ve en el ejemplo, que consiste
en agregar el atributo ActionName("Delete") al método DeleteConfirmed . Ese atributo efectúa la asignación para el
sistema de enrutamiento para que una dirección URL que incluya /Delete/ para una solicitud POST busque el
método DeleteConfirmed .
Otra solución alternativa común para los métodos que tienen nombres y firmas idénticos consiste en cambiar la
firma del método POST artificialmente para incluir un parámetro adicional (sin usar). Es lo que hicimos en una
publicación anterior, cuando agregamos el parámetro notUsed . Podría hacer lo mismo aquí para el método
[HttpPost] Delete :

// POST: Movies/Delete/6
[ValidateAntiForgeryToken]
public async Task<IActionResult> Delete(int id, bool notUsed)

Publicar en Azure
Vea Publicar una aplicación web de ASP.NET Core en Azure App Service con Visual Studio para obtener
instrucciones sobre cómo publicar esta aplicación en Azure con Visual Studio. También se puede publicar la
aplicación desde la línea de comandos.

A N T E R IO R
Creación de una aplicación de ASP.NET Core MVC
con Visual Studio Code
21/06/2018 • 2 minutes to read • Edit Online

En esta serie de tutoriales aprenderá los aspectos básicos de la creación de una aplicación web de ASP.NET Core
MVC con Visual Studio Code.
En este tutorial se muestra el desarrollo web de ASP.NET Core MVC con controladores y vistas. Las páginas de
Razor son una nueva alternativa en ASP.NET Core 2.0 y posteriores, un modelo de programación basado en
páginas que facilita la compilación de interfaces de usuario web y hace que sean más productivas. Se recomienda
probar el tutorial de las páginas de Razor antes que la versión MVC. El tutorial de las páginas de Razor:
Es el método preferido para el desarrollo de nuevas aplicaciones.
Es más fácil de seguir.
Abarca más características.
Si elige este tutorial en lugar de la versión de páginas de Razor, le agradeceremos que nos explique el motivo en
este problema de GitHub.
1. Introducción
2. Agregar un controlador
3. Agregar una vista
4. Agregar un modelo
5. Trabajar con SQLite
6. Vistas y métodos de controlador
7. Agregar búsqueda
8. Agregar un campo nuevo
9. Agregar validación
10. Examinar los métodos Details y Delete
Introducción a ASP.NET Core MVC en macOS, Linux
o Windows
25/06/2018 • 4 minutes to read • Edit Online

Por Rick Anderson


En este tutorial aprenderá los aspectos básicos de la creación de una aplicación web de ASP.NET Core MVC con
Visual Studio Code (VS Code). Para realizar el tutorial debe estar familiarizado con VS Code. Para más
información, vea Getting started with VS Code (Introducción a VS Code) y Visual Studio Code help (Ayuda de
Visual Studio Code).
En este tutorial se muestra el desarrollo web de ASP.NET Core MVC con controladores y vistas. Las páginas de
Razor son una nueva alternativa en ASP.NET Core 2.0 y posteriores, un modelo de programación basado en
páginas que facilita la compilación de interfaces de usuario web y hace que sean más productivas. Se
recomienda probar el tutorial de las páginas de Razor antes que la versión MVC. El tutorial de las páginas de
Razor:
Es el método preferido para el desarrollo de nuevas aplicaciones.
Es más fácil de seguir.
Abarca más características.
Si elige este tutorial en lugar de la versión de páginas de Razor, le agradeceremos que nos explique el motivo en
este problema de GitHub.
Hay tres versiones de este tutorial:
macOS: Creación de una aplicación de ASP.NET Core MVC con Visual Studio para Mac
Windows: Creación de una aplicación de ASP.NET Core MVC con Visual Studio
macOS, Linux y Windows: Creación de una aplicación de ASP.NET Core MVC con Visual Studio Code

Requisitos previos
Install the following:
.NET Core SDK 2.0 or later
Visual Studio Code
C# for Visual Studio Code
.NET Core 2.1 SDK or later
Visual Studio Code
C# for Visual Studio Code

Creación de una aplicación web con dotnet


Desde un terminal, ejecute estos comandos:

mkdir MvcMovie
cd MvcMovie
dotnet new mvc
Abra la carpeta MvcMovie en Visual Studio Code (VS Code) y seleccione el archivo Startup.cs.
Seleccione Sí en el mensaje de advertencia "Required assets to build and debug are missing from
'MvcMovie'. Add them?" (Faltan los activos necesarios para compilar y depurar en 'MvcMovie'. ¿Desea
agregarlos?).
Seleccione Restaurar en el mensaje de información "There are unresolved dependencies" (Hay
dependencias no resueltas).

Presione Depurar (F5) para compilar y ejecutar el programa.


VS Code inicia el servidor web Kestrel y ejecuta la aplicación. Tenga en cuenta que la barra de direcciones
muestra localhost:5000 (y no algo como example.com ). Esto es así porque localhost es el nombre de host
estándar del equipo local.
La plantilla predeterminada proporciona los vínculos Inicio, Acerca de y Contacto, totalmente funcionales. En
la imagen del explorador anterior no se muestran estos vínculos. Según el tamaño del explorador, tendrá que
hacer clic en el icono de navegación para que se muestren.

En la siguiente sección de este tutorial obtendrá información sobre MVC y empezará a escribir código.

Ayuda de Visual Studio Code


Introducción
Depuración
Terminal integrado
Métodos abreviados de teclado
Funciones rápidas de teclado de macOS
Métodos abreviados de teclado de Linux
Windows keyboard shortcuts (Métodos abreviados de teclado de Windows)

NE X T - ADD A (Siguiente - Agregar un controlador)


C ON TROL L E R
Agregar un controlador a una aplicación ASP.NET
Core
25/06/2018 • 10 minutes to read • Edit Online

Por Rick Anderson


El patrón de arquitectura del controlador de vista de modelos (MVC ) separa una aplicación en tres componentes
principales: Modelo, Vista y Controlador. El patrón de MVC ayuda a crear aplicaciones que son más fáciles de
actualizar y probar que las tradicionales aplicaciones monolíticas. Las aplicaciones basadas en MVC contienen:
Modelos: clases que representan los datos de la aplicación. Las clases de modelo usan lógica de validación
para aplicar las reglas de negocio para esos datos. Normalmente, los objetos de modelo recuperan y
almacenan el estado del modelo en una base de datos. En este tutorial, un modelo Movie recupera datos
de películas de una base de datos, los proporciona a la vista o los actualiza. Los datos actualizados se
escriben en una base de datos.
Vistas: las vistas son los componentes que muestran la interfaz de usuario de la aplicación. Por lo general,
esta interfaz de usuario muestra los datos del modelo.
Controladores: las clases que controlan las solicitudes del explorador. Recuperan los datos del modelo y
llaman a plantillas de vistas que devuelven una respuesta. En una aplicación MVC, la vista solo muestra
información; el controlador controla la interacción de los usuarios y los datos que introducen, y responde a
ellos. Por ejemplo, el controlador controla los datos de enrutamiento y los valores de cadena de consulta y
pasa estos valores al modelo. El modelo puede usar estos valores para consultar la base de datos. Por
ejemplo, http://localhost:1234/Home/About tiene datos de enrutamiento de Home (el controlador) y About
(el método de acción para llamar al controlador de inicio). http://localhost:1234/Movies/Edit/5 es una
solicitud para editar la película con ID=5 mediante el controlador de películas. Hablaremos sobre los datos
de enrutamiento más adelante en este tutorial.
El patrón de MVC ayuda a crear aplicaciones que separan los diferentes aspectos de la aplicación (lógica de
entrada, lógica comercial y lógica de la interfaz de usuario), a la vez que proporciona un acoplamiento vago entre
estos elementos. El patrón especifica dónde debe ubicarse cada tipo de lógica en la aplicación. La lógica de la
interfaz de usuario pertenece a la vista. La lógica de entrada pertenece al controlador. La lógica de negocios
pertenece al modelo. Esta separación ayuda a administrar la complejidad al compilar una aplicación, ya que
permite trabajar en uno de los aspectos de la implementación a la vez sin influir en el código de otro. Por ejemplo,
puede trabajar en el código de vista sin depender del código de lógica de negocios.
En esta serie de tutoriales se tratarán estos conceptos y se mostrará cómo usarlos para crear una aplicación de
película. El proyecto de MVC contiene carpetas para controladores y vistas.
En VS Code, haga clic en el icono EXPLORER y luego presione CTRL y haga clic (derecho) en
Controladores > Nuevo archivo y asigne el nombre HelloWorldController.cs al nuevo archivo.
Reemplace el contenido de Controllers/HelloWorldController.cs con lo siguiente:

using Microsoft.AspNetCore.Mvc;
using System.Text.Encodings.Web;

namespace MvcMovie.Controllers
{
public class HelloWorldController : Controller
{
//
// GET: /HelloWorld/

public string Index()


{
return "This is my default action...";
}

//
// GET: /HelloWorld/Welcome/

public string Welcome()


{
return "This is the Welcome action method...";
}
}
}

Cada método public en un controlador puede ser invocado como un punto de conexión HTTP. En el ejemplo
anterior, ambos métodos devuelven una cadena. Observe los comentarios delante de cada método.
Un extremo HTTP es una dirección URL que se puede usar como destino en la aplicación web, como por ejemplo
http://localhost:1234/HelloWorld . Combina el protocolo usado HTTP , la ubicación de red del servidor web
(incluido el puerto TCP ) localhost:1234 y el URI de destino HelloWorld .
El primer comentario dice que se trata de un método HTTP GET que se invoca mediante la anexión de
"/HelloWorld/" a la dirección URL base. El segundo comentario dice que se trata de un método HTTP GET que se
invoca mediante la anexión de "/HelloWorld/Welcome/" a la dirección URL. Más adelante en el tutorial usaremos
el motor de scaffolding para generar métodos HTTP POST .
Ejecute la aplicación en modo de no depuración y anexione "HelloWorld" a la ruta de acceso en la barra de
direcciones. El método Index devuelve una cadena.

MVC invoca las clases del controlador (y los métodos de acción que contienen) en función de la URL entrante. La
lógica de enrutamiento de URL predeterminada que usa MVC emplea un formato similar al siguiente para
determinar qué código se debe invocar:
/[Controller]/[ActionName]/[Parameters]

El formato para el enrutamiento se establece en el método Configure en Startup.cs.

app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});

Cuando se ejecuta la aplicación y no se suministra ningún segmento de dirección URL, de manera


predeterminada se usa el controlador "Home" y el método "Index" especificados en la línea de plantilla resaltada
arriba.
El primer segmento de dirección URL determina la clase de controlador que se va a ejecutar. De modo que
localhost:xxxx/HelloWorld se asigna a la clase HelloWorldController . La segunda parte del segmento de
dirección URL determina el método de acción en la clase. De modo que localhost:xxxx/HelloWorld/Index podría
provocar que se ejecute el método Index de la clase HelloWorldController . Tenga en cuenta que solo es
necesario navegar a localhost:xxxx/HelloWorld para que se llame al método Index de manera predeterminada.
Esto es porque Index es el método predeterminado al que se llamará en un controlador si no se especifica
explícitamente un nombre de método. La tercera parte del segmento de dirección URL ( id ) es para los datos de
ruta. Veremos los datos de ruta más adelante en este tutorial.
Vaya a http://localhost:xxxx/HelloWorld/Welcome . El método Welcome se ejecuta y devuelve la cadena "This is the
Welcome action method..." (Este es el método de acción de bienvenida). Para esta dirección URL, el controlador es
HelloWorld y Welcome es el método de acción. Todavía no ha usado el elemento [Parameters] de la dirección
URL.
Modifique el código para pasar cierta información del parámetro desde la dirección URL al controlador. Por
ejemplo: /HelloWorld/Welcome?name=Rick&numtimes=4 . Cambie el método Welcome para que incluya dos parámetros,
como se muestra en el código siguiente.

// GET: /HelloWorld/Welcome/
// Requires using System.Text.Encodings.Web;
public string Welcome(string name, int numTimes = 1)
{
return HtmlEncoder.Default.Encode($"Hello {name}, NumTimes is: {numTimes}");
}

El código anterior:
Usa la característica de parámetro opcional de C# para indicar que el parámetro numTimes tiene el valor
predeterminado 1 si no se pasa ningún valor para ese parámetro.
Usa HtmlEncoder.Default.Encode para proteger la aplicación de entradas malintencionadas (es decir,
JavaScript).
Usa cadenas interpoladas.
Ejecute la aplicación y navegue a:
http://localhost:xxxx/HelloWorld/Welcome?name=Rick&numtimes=4

(Reemplace xxxx con el número de puerto). Puede probar valores diferentes para name y numtimes en la dirección
URL. El sistema de enlace de modelos de MVC asigna automáticamente los parámetros con nombre de la cadena
de consulta en la barra de dirección a los parámetros del método. Vea Model Binding (Enlace de modelos) para
más información.

En la ilustración anterior, el segmento de dirección URL ( Parameters ) no se usa, y los parámetros name y
numTimes se pasan como cadenas de consulta. El ? (signo de interrogación) en la dirección URL anterior es un
separador y le siguen las cadenas de consulta. El carácter & separa las cadenas de consulta.
Reemplace el método Welcome con el código siguiente:

public string Welcome(string name, int ID = 1)


{
return HtmlEncoder.Default.Encode($"Hello {name}, ID: {ID}");
}

Ejecute la aplicación y escriba la dirección URL siguiente: http://localhost:xxx/HelloWorld/Welcome/3?name=Rick

Esta vez el tercer segmento de dirección URL coincide con el parámetro de ruta id . El método Welcome contiene
un parámetro id que coincide con la plantilla de dirección URL en el método MapRoute . El ? del final (en id? )
indica que el parámetro id es opcional.

app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});

En estos ejemplos, el controlador ha realizado la parte "VC" de MVC, es decir, el trabajo de vista y de controlador.
El controlador devuelve HTML directamente. Por lo general, no es aconsejable que los controles devuelvan HTML
directamente, porque resulta muy complicado de programar y mantener. En su lugar, se suele usar un archivo de
plantilla de vista de Razor independiente para ayudar a generar la respuesta HTML. Haremos esto en el siguiente
tutorial.

A N T E R IO R : A G R E G A R U N S IG U IE N T E : A G R E G A R U N A
C ON TROL A D OR V IS T A
Adición de una vista en una aplicación de ASP.NET
Core MVC
25/06/2018 • 14 minutes to read • Edit Online

Por Rick Anderson


En esta sección, se modificará la clase HelloWorldController para usar los archivos de plantilla de vista Razor con
el objetivo de encapsular correctamente el proceso de generar respuestas HTML a un cliente.
Para crear un archivo de plantilla de vista se usa Razor. Las plantillas de vista basadas en Razor tienen una
extensión de archivo .cshtml. Ofrecen una forma elegante de crear un resultado HTML mediante C#.
Actualmente, el método Index devuelve una cadena con un mensaje que está codificado de forma rígida en la
clase de controlador. En la clase HelloWorldController , reemplace el método Index por el siguiente código:

public IActionResult Index()


{
return View();
}

El código anterior devuelve un objeto View . Usa una plantilla de vista para generar una respuesta HTML al
explorador. Los métodos de controlador (también conocidos como métodos de acción), como el método Index
anterior, suelen devolver IActionResult o una clase derivada de ActionResult , en lugar de un tipo como una
cadena.
Agregue una vista Index para el HelloWorldController .
Agregue una nueva carpeta denominada Views/HelloWorld.
Agregue un nuevo archivo a la carpeta Views/HelloWorld y asígnele el nombre Index.cshtml.
Reemplace el contenido del archivo de vista de Razor Views/HelloWorld/Index.cshtml con lo siguiente:

@{
ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>Hello from our View Template!</p>

Navegue a http://localhost:xxxx/HelloWorld . El método Index en HelloWorldController no hizo mucho; ejecutó


la instrucción return View(); , que especificaba que el método debe usar un archivo de plantilla de vista para
representar una respuesta al explorador. Como no especificó expresamente el nombre del archivo de plantilla de
vista, MVC usó de manera predeterminada el archivo de vista Index.cshtml de la carpeta /Views/HelloWorld. La
imagen siguiente muestra la cadena "Hello from our View Template!" (Hola desde nuestra plantilla de vista)
codificada de forma rígida en la vista.
Si la ventana del explorador es pequeña (por ejemplo en un dispositivo móvil), es conveniente que alterne (pulse)
el botón de navegación de arranque en la esquina superior derecha para ver los vínculos Home (Inicio), About
(Acerca de) y Contact (Contacto).

Cambiar vistas y páginas de diseño


Pulse los vínculos de menú: MvcMovie (Película de MVC ), Home (Inicio), About (Acerca de). Cada página
muestra el mismo diseño de menú. El diseño de menú se implementa en el archivo Views/Shared/_Layout.cshtml.
Abra el archivo Views/Shared/_Layout.cshtml.
Las plantillas de diseño permiten especificar el diseño del contenedor HTML del sitio en un solo lugar y, después,
aplicarlo en varias páginas del sitio. Busque la línea @RenderBody() . RenderBody es un marcador de posición
donde se mostrarán todas las páginas específicas de vista que cree, encapsuladas en la página de diseño. Por
ejemplo, si selecciona el vínculo About (Acerca de), la vista Views/Home/About.cshtml se representa dentro
del método RenderBody .

Cambiar el título y el vínculo del menú en el archivo de diseño


En el elemento de título, cambie MvcMovie por Movie App . Cambie el texto del delimitador en la plantilla de
diseño de MvcMovie a Movie App y el controlador de Home a Movies como se resalta aquí:

[!code-html]
[!code-html]

WARNING
Aún no hemos implementado el controlador Movies , por lo que si hace clic en ese vínculo, obtendrá un error 404 (no
encontrado).

Guarde los cambios y pulse en el vínculo About (Acerca de). Observe cómo el título de la pestaña del explorador
muestra ahora About - Movie App (Acerca de - Aplicación de película) en lugar de About - Mvc Movie (Acerca
de - Aplicación de MVC ):

Pulse el vínculo Contacto y observe que el texto del título y el delimitador también muestran Movie App.
Hemos realizado el cambio una vez en la plantilla de diseño y hemos conseguido que todas las páginas del sitio
reflejen el nuevo texto de vínculo y el nuevo título.
Examine el archivo Views/_ViewStart.cshtml:

@{
Layout = "_Layout";
}

El archivo Views/_ViewStart.cshtml trae el archivo Views/Shared/_Layout.cshtml a cada vista. Puede usar la


propiedad Layout para establecer una vista de diseño diferente o establecerla en null para que no se use
ningún archivo de diseño.
Cambie el título de la vista Index .
Abra Views/HelloWorld/Index.cshtml. Los cambios se pueden hacer en dos lugares:
El texto que aparece en el título del explorador.
El encabezado secundario (elemento <h2> ).
Haremos que sean algo diferentes para que pueda ver qué parte del código cambia cada área de la aplicación.

@{
ViewData["Title"] = "Movie List";
}

<h2>My Movie List</h2>

<p>Hello from our View Template!</p>

En el código anterior, ViewData["Title"] = "Movie List"; establece la propiedad Title del diccionario ViewData
en "Movie List" (Lista de películas). La propiedad Title se usa en el elemento HTML <title> en la página de
diseño:

<title>@ViewData["Title"] - Movie App</title>

Guarde el cambio y navegue a http://localhost:xxxx/HelloWorld . Tenga en cuenta que el título del explorador, el
encabezado principal y los encabezados secundarios han cambiado. (Si no ve los cambios en el explorador, es
posible que esté viendo contenido almacenado en caché. Presione Ctrl+F5 en el explorador para forzar que se
cargue la respuesta del servidor). El título del explorador se crea con ViewData["Title"] , que se definió en la
plantilla de vista Index.cshtml y el texto "- Movie App" (-Aplicación de película) que se agregó en el archivo de
diseño.
Observe también cómo el contenido de la plantilla de vista Index.cshtml se fusionó con la plantilla de vista
Views/Shared/_Layout.cshtml y se envió una única respuesta HTML al explorador. Con las plantillas de diseño es
realmente fácil hacer cambios para que se apliquen en todas las páginas de la aplicación. Para saber más, vea
Layout (Diseño).

Nuestra pequeña cantidad de "datos", en este caso, el mensaje "Hello from our View Template!" (Hola desde
nuestra plantilla de vista), están codificados de forma rígida. La aplicación de MVC tiene una "V" (vista) y ha
obtenido una "C" (controlador), pero todavía no tiene una "M" (modelo).

Pasar datos del controlador a la vista


Las acciones del controlador se invocan en respuesta a una solicitud de dirección URL entrante. Una clase de
controlador es donde se escribe el código que controla las solicitudes entrantes del explorador. El controlador
recupera datos de un origen de datos y decide qué tipo de respuesta devolverá al explorador. Las plantillas de
vista se pueden usar desde un controlador para generar y dar formato a una respuesta HTML al explorador.
Los controladores se encargan de proporcionar los datos necesarios para que una plantilla de vista represente una
respuesta. Procedimiento recomendado: las plantillas de vista no deben realizar lógica de negocios ni interactuar
directamente con una base de datos. En su lugar, una plantilla de vista debe funcionar solo con los datos que le
proporciona el controlador. Mantener esta "separación de intereses" ayuda a mantener el código limpio, fácil de
probar y de mantener.
Actualmente, el método Welcome de la clase HelloWorldController toma un parámetro name y ID , y luego
obtiene los valores directamente en el explorador. En lugar de que el controlador represente esta respuesta como
una cadena, cambie el controlador para que use una plantilla de vista. La plantilla de vista genera una respuesta
dinámica, lo que significa que se deben pasar las partes de datos adecuadas desde el controlador a la vista para
que se genere la respuesta. Para hacerlo, indique al controlador que coloque los datos dinámicos (parámetros)
que necesita la plantilla de vista en un diccionario ViewData al que luego pueda obtener acceso la plantilla de
vista.
Vuelva al archivo HelloWorldController.cs y cambie el método Welcome para agregar un valor Message y
NumTimes al diccionario ViewData . El diccionario ViewData es un objeto dinámico, lo que significa que puede
colocar en él todo lo que quiera; el objeto ViewData no tiene ninguna propiedad definida hasta que coloca algo
dentro de él. El sistema de enlace de modelos de MVC asigna automáticamente los parámetros con nombre (
name y numTimes ) de la cadena de consulta en la barra de dirección a los parámetros del método. El archivo
HelloWorldController.cs completo tiene este aspecto:

using Microsoft.AspNetCore.Mvc;
using System.Text.Encodings.Web;

namespace MvcMovie.Controllers
{
public class HelloWorldController : Controller
{
public IActionResult Index()
{
return View();
}

public IActionResult Welcome(string name, int numTimes = 1)


{
ViewData["Message"] = "Hello " + name;
ViewData["NumTimes"] = numTimes;

return View();
}
}
}

El objeto de diccionario ViewData contiene datos que se pasarán a la vista.


Cree una plantilla de vista principal denominada Views/HelloWorld/Welcome.cshtml.
Se creará un bucle en la vista Welcome.cshtml que muestra "Hello" (Hola) NumTimes . Reemplace el contenido de
Views/HelloWorld/Welcome.cshtml con lo siguiente:
@{
ViewData["Title"] = "Welcome";
}

<h2>Welcome</h2>

<ul>
@for (int i = 0; i < (int)ViewData["NumTimes"]; i++)
{
<li>@ViewData["Message"]</li>
}
</ul>

Guarde los cambios y vaya a esta dirección URL:


http://localhost:xxxx/HelloWorld/Welcome?name=Rick&numtimes=4

Los datos se toman de la dirección URL y se pasan al controlador mediante el enlazador de modelos MVC. El
controlador empaqueta los datos en un diccionario ViewData y pasa ese objeto a la vista. Después, la vista
representa los datos como HTML en el explorador.

En el ejemplo anterior, usamos el diccionario ViewData para pasar datos del controlador a una vista. Más adelante
en el tutorial usaremos un modelo de vista para pasar datos de un controlador a una vista. El enfoque del modelo
de vista que consiste en pasar datos suele ser más preferible que el enfoque de diccionario ViewData . Para saber
más, vea ViewModel vs ViewData vs ViewBag vs TempData vs Session in MVC (ViewModel, ViewData, ViewBag,
TempData y Session en MVC ).
Bueno, todo esto era un tipo de "M" para el modelo, pero no el tipo de base de datos. Vamos a aprovechar lo que
hemos aprendido para crear una base de datos de películas.
A N T E R IO R : A G R E G A R U N S IG U IE N T E : A G R E G A R U N
C ON TROL A D OR M ODELO
Agregar un modelo a una aplicación de ASP.NET
Core MVC
25/06/2018 • 6 minutes to read • Edit Online

Adición de un modelo a una aplicación de ASP.NET Core


MVC
Por Rick Anderson y Tom Dykstra
En esta sección, agregará algunas clases para administrar películas en una base de datos. Estas clases serán el
elemento "Model" de la aplicación MVC.
Estas clases se usan con Entity Framework Core (EF Core) para trabajar con una base de datos. EF Core es un
marco de trabajo de asignación relacional de objetos (ORM ) que simplifica el código de acceso de datos que se
debe escribir. EF Core es compatible con muchos motores de base de datos.
Las clases de modelo que se crean se conocen como clases POCO (del inglés "plain-old CLR objects", objetos
CLR antiguos sin formato) porque no tienen ninguna dependencia de EF Core. Simplemente definen las
propiedades de los datos que se almacenan en la base de datos.
En este tutorial, se escriben primero las clases de modelo y EF Core creará la base de datos. Hay un enfoque
alternativo, que no se trata aquí, que consiste en generar clases de modelo a partir de una base de datos existente.
Para más información sobre este enfoque, vea ASP.NET Core - Existing Database (ASP.NET Core - base de datos
existente).

Agregar una clase de modelo de datos


Agregue una clase a la carpeta Modelos denominada Movie.cs.
Agregue el código siguiente al archivo Modelos/Movie.cs:

using System;

namespace MvcMovie.Models
{
public class Movie
{
public int ID { get; set; }
public string Title { get; set; }
public DateTime ReleaseDate { get; set; }
public string Genre { get; set; }
public decimal Price { get; set; }
}
}

La base de datos requiere el campo ID para la clave principal.


Compile la aplicación para comprobar que no tiene ningún error y que ha agregado un modelo a la aplicación
MVC.

Preparar el proyecto para la técnica scaffolding


Agregue los siguientes paquetes de NuGet resaltados al archivo MvcMovie.csproj:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.0.0" />
</ItemGroup>
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.0" />
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.0"
/>
</ItemGroup>
</Project>

Guarde el archivo y seleccione Restaurar en el mensaje de información "Hay dependencias no resueltas".


Cree un archivo Modelos/MvcMovieContext.cs y agregue la siguiente clase MvcMovieContext :

using Microsoft.EntityFrameworkCore;

namespace MvcMovie.Models
{
public class MvcMovieContext : DbContext
{
public MvcMovieContext (DbContextOptions<MvcMovieContext> options)
: base(options)
{
}

public DbSet<MvcMovie.Models.Movie> Movie { get; set; }


}
}

Abra el archivo Startup.cs y agregue dos instrucciones using:

using Microsoft.EntityFrameworkCore;
using MvcMovie.Models;

namespace MvcMovie
{
public class Startup
{

Agregue el contexto de base de datos al archivo Startup.cs:

public void ConfigureServices(IServiceCollection services)


{
// Add framework services.
services.AddMvc();

services.AddDbContext<MvcMovieContext>(options =>
options.UseSqlite("Data Source=MvcMovie.db"));

Esto indica a Entity Framework las clases de modelo que se incluyen en el modelo de datos. Se va a definir
un conjunto de entidades de objetos Movie, que se representarán en la base de datos como una tabla de
películas.
Compile el proyecto para comprobar que no hay ningún error.

Aplicar la técnica scaffolding a MovieController


Abra una ventana de terminal en la carpeta del proyecto y ejecute los siguientes comandos:

dotnet restore
dotnet aspnet-codegenerator controller -name MoviesController -m Movie -dc MvcMovieContext --
relativeFolderPath Controllers --useDefaultLayout --referenceScriptLibraries

El motor de scaffolding crea lo siguiente:


Un controlador de películas (Controllers/MoviesController.cs)
Archivos de vistas de Razor para las páginas Crear, Eliminar, Detalles, Editar e Índice (Views/Movies/*.cshtml)
La creación automática de vistas y métodos de acción CRUD (crear, leer, actualizar y eliminar) se conoce como
scaffolding. Pronto contará con una aplicación web totalmente funcional que le permitirá administrar una base de
datos de películas.

Realizar la migración inicial


Desde la línea de comandos, ejecute los comandos CLI de .NET Core:

dotnet ef migrations add InitialCreate


dotnet ef database update

El comando dotnet ef migrations add InitialCreate genera el código para crear el esquema de base de datos
inicial. El esquema se basa en el modelo especificado en DbContext (en el archivo Models/MvcMovieContext.cs). El
argumento Initial se usa para asignar nombre a las migraciones. Puede usar cualquier nombre, pero se suele
elegir uno que describa la migración. Vea Introduction to migrations (Introducción a las migraciones) para
obtener más información.
El comando dotnet ef database update ejecuta el método Up en el archivo Migrations/<time-
stamp>_InitialCreate.cs, con lo que se crea la base de datos.

Prueba de la aplicación
Ejecute la aplicación y pulse en el vínculo Mvc Movie (Película Mvc).
Pulse Create New (Crear nueva) y cree una película.
Es posible no que pueda escribir comas ni puntos decimales en el campo Price . Para admitir la validación
de jQuery para configuraciones regionales distintas del inglés que usan una coma (",") en lugar de un
punto decimal y formatos de fecha distintos de Estados Unidos, debe seguir unos pasos globalizar la
aplicación. Consulte https://github.com/aspnet/Docs/issues/4076 y recursos adicionales para obtener más
información. Por ahora, tan solo debe escribir números enteros como 10.
En algunas configuraciones regionales debe especificar el formato de fecha. Vea el código que aparece
resaltado a continuación.

using System;
using System.ComponentModel.DataAnnotations;

namespace MvcMovie.Models
{
public class Movie
{
public int ID { get; set; }
public string Title { get; set; }
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime ReleaseDate { get; set; }
public string Genre { get; set; }
public decimal Price { get; set; }
}
}
Hablaremos sobre DataAnnotations más adelante en el tutorial.
Al pulsar en Crear el formulario se envía al servidor, donde se guarda la información de la película en una base de
datos. La aplicación redirige a la URL /Movies, donde se muestra la información de la película que acaba de crear.

Cree un par de entradas más de película. Compruebe que todos los vínculos Editar, Detalles y Eliminar son
funcionales.
Ahora dispone de una base de datos y de páginas para mostrar, editar, actualizar y eliminar datos. En el siguiente
tutorial trabajaremos con la base de datos.
Recursos adicionales
Aplicaciones auxiliares de etiquetas
Globalización y localización

A N T E R IO R : A G R E G A R U N A S IG U IE N T E : T R A B A J A R C O N
V IS T A S Q L IT E
Trabajar con SQLite en un proyecto de ASP.NET
Core MVC
25/06/2018 • 2 minutes to read • Edit Online

Por Rick Anderson


El objeto MvcMovieContext controla la tarea de conexión a la base de datos y asignación de objetos Movie a los
registros de la base de datos. El contexto de base de datos se registra con el contenedor de inserción de
dependencias en el método ConfigureServices del archivo Startup.cs:

public void ConfigureServices(IServiceCollection services)


{
// Add framework services.
services.AddMvc();

services.AddDbContext<MvcMovieContext>(options =>
options.UseSqlite("Data Source=MvcMovie.db"));

SQLite
Según la información del sitio web de SQLite:

SQLite es un motor de base de datos SQL independiente, de alta confiabilidad, insertado, con características
completas y dominio público. SQLite es el motor de base de datos más usado en el mundo.

Existen muchas herramientas de terceros que se pueden descargar para administrar y ver una base de datos de
SQLite. La imagen de abajo es de DB Browser for SQLite. Si tiene una herramienta favorita de SQLite, deje un
comentario sobre lo que le gusta de ella.
Inicializar la base de datos
Cree una nueva clase denominada SeedData en la carpeta Models. Reemplace el código generado con el
siguiente:
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Linq;

namespace MvcMovie.Models
{
public static class SeedData
{
public static void Initialize(IServiceProvider serviceProvider)
{
using (var context = new MvcMovieContext(
serviceProvider.GetRequiredService<DbContextOptions<MvcMovieContext>>()))
{
// Look for any movies.
if (context.Movie.Any())
{
return; // DB has been seeded
}

context.Movie.AddRange(
new Movie
{
Title = "When Harry Met Sally",
ReleaseDate = DateTime.Parse("1989-1-11"),
Genre = "Romantic Comedy",
Price = 7.99M
},

new Movie
{
Title = "Ghostbusters ",
ReleaseDate = DateTime.Parse("1984-3-13"),
Genre = "Comedy",
Price = 8.99M
},

new Movie
{
Title = "Ghostbusters 2",
ReleaseDate = DateTime.Parse("1986-2-23"),
Genre = "Comedy",
Price = 9.99M
},

new Movie
{
Title = "Rio Bravo",
ReleaseDate = DateTime.Parse("1959-4-15"),
Genre = "Western",
Price = 3.99M
}
);
context.SaveChanges();
}
}
}
}

Si hay alguna película en la base de datos, se devuelve el inicializador.


if (context.Movie.Any())
{
return; // DB has been seeded.
}

Agregar el inicializador
Agregue el inicializador al método Main en el archivo Program.cs:
[!code-csharp]
[!code-csharp]
Prueba de la aplicación
Elimine todos los registros de la base de datos (para que se ejecute el método de inicialización). Detenga e inicie la
aplicación para inicializar la base de datos.
La aplicación muestra los datos inicializados.

A N T E R IO R : A G R E G A R U N S IG U IE N T E : V IS T A S Y M É T O D O S D E
M ODELO C ON TROL A D OR
Vistas y métodos de controlador en ASP.NET Core
25/06/2018 • 15 minutes to read • Edit Online

Por Rick Anderson


La aplicación de películas pinta bien, pero la presentación no es ideal. No queremos ver la hora (12:00:00 a.m. en
la imagen siguiente) y ReleaseDate deben ser dos palabras.

Abra el archivo Models/Movie.cs y agregue las líneas resaltadas que se muestran a continuación:

using System;
using System.ComponentModel.DataAnnotations;

namespace MvcMovie.Models
{
public class Movie
{
public int ID { get; set; }
public string Title { get; set; }

[Display(Name = "Release Date")]


[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }
public string Genre { get; set; }
public decimal Price { get; set; }
}
}

Compile y ejecute la aplicación.


En el próximo tutorial hablaremos de DataAnnotations. El atributo Display especifica qué se muestra como
nombre de un campo (en este caso, "Release Date" en lugar de "ReleaseDate"). El atributo DataType especifica el
tipo de los datos (Date), así que la información de hora almacenada en el campo no se muestra.
Vaya al controlador Movies y mantenga el puntero del mouse sobre un vínculo Edit (Editar) para ver la dirección
URL de destino.

Los vínculos Edit (Editar), Details (Detalles) y Delete (Eliminar) se generan mediante la aplicación auxiliar de
etiquetas de delimitador de MVC Core en el archivo Views/Movies/Index.cshtml.

<a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |


<a asp-action="Details" asp-route-id="@item.ID">Details</a> |
<a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>

Las aplicaciones auxiliares de etiquetas permiten que el código de servidor participe en la creación y la
representación de elementos HTML en archivos de Razor. En el código anterior, AnchorTagHelper genera
dinámicamente el valor del atributo HTML href a partir del identificador de ruta y el método de acción del
controlador. Use Ver código fuente en su explorador preferido o use las herramientas de desarrollo para
examinar el marcado generado. A continuación se muestra una parte del HTML generado:

<td>
<a href="/Movies/Edit/4"> Edit </a> |
<a href="/Movies/Details/4"> Details </a> |
<a href="/Movies/Delete/4"> Delete </a>
</td>

Recupere el formato para el enrutamiento establecido en el archivo Startup.cs:


app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});

ASP.NET Core traduce http://localhost:1234/Movies/Edit/4 en una solicitud al método de acción Edit del
controlador Movies con el parámetro Id de 4. (Los métodos de controlador también se denominan métodos de
acción).
Las aplicaciones auxiliares de etiquetas son una de las nuevas características más populares de ASP.NET Core.
Para más información, vea Recursos adicionales.
Abra el controlador Movies y examine los dos métodos de acción Edit . En el código siguiente se muestra el
método HTTP GET Edit , que captura la película y rellena el formulario de edición generado por el archivo de Razor
Edit.cshtml.

// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
return NotFound();
}

var movie = await _context.Movie.SingleOrDefaultAsync(m => m.ID == id);


if (movie == null)
{
return NotFound();
}
return View(movie);
}

En el código siguiente se muestra el método HTTP POST Edit , que procesa los valores de película publicados:
// POST: Movies/Edit/5
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("ID,Title,ReleaseDate,Genre,Price")] Movie movie)
{
if (id != movie.ID)
{
return NotFound();
}

if (ModelState.IsValid)
{
try
{
_context.Update(movie);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!MovieExists(movie.ID))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction("Index");
}
return View(movie);
}

El atributo [Bind] es una manera de proteger contra el exceso de publicación. Solo debe incluir propiedades en el
atributo [Bind] que quiera cambiar. Para más información, vea Protección del controlador frente al exceso de
publicación. ViewModels ofrece un enfoque alternativo para evitar el exceso de publicaciones.
Observe que el segundo método de acción Edit va precedido del atributo [HttpPost] .
// POST: Movies/Edit/5
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("ID,Title,ReleaseDate,Genre,Price")] Movie movie)
{
if (id != movie.ID)
{
return NotFound();
}

if (ModelState.IsValid)
{
try
{
_context.Update(movie);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!MovieExists(movie.ID))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction("Index");
}
return View(movie);
}

El atributo HttpPost especifica que este método Edit se puede invocar solamente para solicitudes POST . Podría
aplicar el atributo [HttpGet] al primer método de edición, pero no es necesario hacerlo porque [HttpGet] es el
valor predeterminado.
El atributo ValidateAntiForgeryToken se usa para impedir la falsificación de una solicitud y se empareja con un
token antifalsificación generado en el archivo de vista de edición (Views/Movies/Edit.cshtml). El archivo de vista de
edición genera el token antifalsificación con la aplicación auxiliar de etiquetas de formulario.

<form asp-action="Edit">

La aplicación auxiliar de etiquetas de formulario genera un token antifalsificación oculto que debe coincidir con el
token antifalsificación generado por [ValidateAntiForgeryToken] en el método Edit del controlador Movies. Para
más información, vea Prevención de ataques de falsificación de solicitudes.
El método HttpGet Edit toma el parámetro ID de la película, busca la película con el método
SingleOrDefaultAsync de Entity Framework y devuelve la película seleccionada a la vista de edición. Si no se
encuentra una película, se devuelve NotFound (HTTP 404).
// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
return NotFound();
}

var movie = await _context.Movie.SingleOrDefaultAsync(m => m.ID == id);


if (movie == null)
{
return NotFound();
}
return View(movie);
}

Cuando el sistema de scaffolding creó la vista de edición, examinó la clase Movie y creó código para representar
los elementos <label> y <input> para cada propiedad de la clase. En el ejemplo siguiente se muestra la vista de
edición que generó el sistema de scaffolding de Visual Studio:
@model MvcMovie.Models.Movie

@{
ViewData["Title"] = "Edit";
}

<h2>Edit</h2>

<form asp-action="Edit">
<div class="form-horizontal">
<h4>Movie</h4>
<hr />
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="ID" />
<div class="form-group">
<label asp-for="Title" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Title" class="form-control" />
<span asp-validation-for="Title" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="ReleaseDate" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="ReleaseDate" class="form-control" />
<span asp-validation-for="ReleaseDate" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="Genre" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Genre" class="form-control" />
<span asp-validation-for="Genre" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="Price" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Price" class="form-control" />
<span asp-validation-for="Price" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</div>
</div>
</form>

<div>
<a asp-action="Index">Back to List</a>
</div>

@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

Observe cómo la plantilla de vista tiene una instrucción @model MvcMovie.Models.Movie en la parte superior del
archivo. @model MvcMovie.Models.Movie especifica que la vista espera que el modelo de la plantilla de vista sea del
tipo Movie .
El código con scaffolding usa varios métodos de aplicación auxiliar de etiquetas para simplificar el marcado
HTML. La aplicación auxiliar de etiquetas muestra el nombre del campo: "Title" (Título), "ReleaseDate" (Fecha de
lanzamiento), "Genre" (Género) o "Price" (Precio). La aplicación auxiliar de etiquetas de entrada representa un
elemento HTML <input> . La aplicación auxiliar de etiquetas de validación muestra cualquier mensaje de
validación asociado a esa propiedad.
Ejecute la aplicación y navegue a la URL /Movies . Haga clic en un vínculo Edit (Editar). En el explorador, vea el
código fuente de la página. El código HTML generado para el elemento <form> se muestra abajo.

<form action="/Movies/Edit/7" method="post">


<div class="form-horizontal">
<h4>Movie</h4>
<hr />
<div class="text-danger" />
<input type="hidden" data-val="true" data-val-required="The ID field is required." id="ID" name="ID"
value="7" />
<div class="form-group">
<label class="control-label col-md-2" for="Genre" />
<div class="col-md-10">
<input class="form-control" type="text" id="Genre" name="Genre" value="Western" />
<span class="text-danger field-validation-valid" data-valmsg-for="Genre" data-valmsg-
replace="true"></span>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-2" for="Price" />
<div class="col-md-10">
<input class="form-control" type="text" data-val="true" data-val-number="The field Price must
be a number." data-val-required="The Price field is required." id="Price" name="Price" value="3.99" />
<span class="text-danger field-validation-valid" data-valmsg-for="Price" data-valmsg-
replace="true"></span>
</div>
</div>
<!-- Markup removed for brevity -->
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</div>
</div>
<input name="__RequestVerificationToken" type="hidden"
value="CfDJ8Inyxgp63fRFqUePGvuI5jGZsloJu1L7X9le1gy7NCIlSduCRx9jDQClrV9pOTTmqUyXnJBXhmrjcUVDJyDUMm7-
MF_9rK8aAZdRdlOri7FmKVkRe_2v5LIHGKFcTjPrWPYnc9AdSbomkiOSaTEg7RU" />
</form>

Los elementos <input> se muestran en un elemento HTML <form> cuyo atributo action se establece para
publicar en la dirección URL /Movies/Edit/id . Los datos del formulario se publicarán en el servidor cuando se
haga clic en el botón Save . La última línea antes del cierre del elemento </form> muestra el token XSRF oculto
generado por la aplicación auxiliar de etiquetas de formulario.

Procesamiento de la solicitud POST


En la siguiente lista se muestra la versión [HttpPost] del método de acción Edit .
// POST: Movies/Edit/5
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("ID,Title,ReleaseDate,Genre,Price")] Movie movie)
{
if (id != movie.ID)
{
return NotFound();
}

if (ModelState.IsValid)
{
try
{
_context.Update(movie);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!MovieExists(movie.ID))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction("Index");
}
return View(movie);
}

El atributo [ValidateAntiForgeryToken] valida el token XSRF oculto generado por el generador de tokens
antifalsificación en la herramienta auxiliar de etiquetas de formulario.
El sistema de enlace de modelos toma los valores de formulario publicados y crea un objeto Movie que se pasa
como el parámetro movie . El método ModelState.IsValid comprueba que los datos presentados en el formulario
pueden usarse para modificar (editar o actualizar) un objeto Movie . Si los datos son válidos, se guardan. Los
datos de película actualizados (o modificados) se guardan en la base de datos mediante una llamada al método
SaveChangesAsync del contexto de base de datos. Después de guardar los datos, el código redirige al usuario al
método de acción Index de la clase MoviesController , que muestra la colección de películas, incluidos los
cambios que se acaban de hacer.
Antes de que el formulario se envíe al servidor, la validación del lado cliente comprueba cualquier regla de
validación en los campos. Si hay errores de validación, se muestra un mensaje de error y no se publica el
formulario. Si JavaScript está deshabilitado, no dispondrá de la validación del lado cliente, sino que el servidor
detectará los valores publicados que no son válidos y los valores de formulario se volverán a mostrar con
mensajes de error. Más adelante en el tutorial se examina la validación de modelos con más detalle. La aplicación
auxiliar de etiquetas de validación en la plantilla de vista Views/Movies/Edit.cshtml se encarga de mostrar los
mensajes de error correspondientes.
Todos los métodos HttpGet del controlador de películas siguen un patrón similar. Obtienen un objeto de película
(o una lista de objetos, en el caso de Index ) y pasan el objeto (modelo) a la vista. El método Create pasa un
objeto de película vacío a la vista Create . Todos los métodos que crean, editan, eliminan o modifican los datos lo
hacen en la sobrecarga [HttpPost] del método. La modificación de datos en un método HTTP GET supone un
riesgo de seguridad. La modificación de datos en un método HTTP GET también infringe procedimientos
recomendados de HTTP y el patrón de arquitectura REST, que especifica que las solicitudes GET no deben
cambiar el estado de la aplicación. En otras palabras, realizar una operación GET debería ser una operación segura
sin efectos secundarios, que no modifica los datos persistentes.

Recursos adicionales
Globalización y localización
Introducción a las aplicaciones auxiliares de etiquetas
Creación de aplicaciones auxiliares de etiquetas
Prevención de ataques de falsificación de solicitudes
Protección del controlador frente al exceso de publicación
ViewModels
Aplicación auxiliar de etiquetas de formulario
Aplicación auxiliar de etiquetas de entrada
Aplicación auxiliar de etiquetas de elementos de etiqueta
Aplicación auxiliar de etiquetas de selección
Aplicación auxiliar de etiquetas de validación

A N T E R IO R : T R A B A J A R C O N S IG U IE N T E : A G R E G A R
S Q L IT E BÚSQUEDA
Adición de una búsqueda en una aplicación de
ASP.NET Core MVC
25/06/2018 • 12 minutes to read • Edit Online

Por Rick Anderson


En esta sección agregará capacidad de búsqueda para el método de acción Index que permite buscar películas
por género o nombre.
Actualice el método Index con el código siguiente:

public async Task<IActionResult> Index(string searchString)


{
var movies = from m in _context.Movie
select m;

if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}

return View(await movies.ToListAsync());


}

La primera línea del método de acción Index crea una consulta LINQ para seleccionar las películas:

var movies = from m in _context.Movie


select m;

En este momento solo se define la consulta, no se ejecuta en la base de datos.


Si el parámetro searchString contiene una cadena, la consulta de películas se modifica para filtrar según el valor
de la cadena de búsqueda:

if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}

El código s => s.Title.Contains() anterior es una expresión Lambda. Las lambdas se usan en consultas LINQ
basadas en métodos como argumentos para métodos de operador de consulta estándar, tales como el método
Where o Contains (usado en el código anterior). Las consultas LINQ no se ejecutan cuando se definen ni cuando
se modifican mediante una llamada a un método, como Where , Contains u OrderBy . En su lugar, se aplaza la
ejecución de la consulta. Esto significa que la evaluación de una expresión se aplaza hasta que su valor realizado se
repita realmente o se llame al método ToListAsync . Para más información sobre la ejecución de consultas en
diferido, vea Ejecución de la consulta.
Nota: El método Contains se ejecuta en la base de datos, no en el código de c# que se muestra arriba. La
distinción entre mayúsculas y minúsculas en la consulta depende de la base de datos y la intercalación. En SQL
Server, Contains se asigna a SQL LIKE, que distingue entre mayúsculas y minúsculas. En SQLite, con la
intercalación predeterminada, se distingue entre mayúsculas y minúsculas.
Navegue a /Movies/Index . Anexe una cadena de consulta como ?searchString=Ghost a la dirección URL. Se
muestran las películas filtradas.

Si se cambia la firma del método Index para que tenga un parámetro con el nombre id , el parámetro id
coincidirá con el marcador {id} opcional para el conjunto de rutas predeterminado en Startup.cs.

app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});

Nota: SQLlite distingue mayúsculas de minúsculas, por lo que tendrá que buscar "Ghost" y no "ghost".
El método Index anterior:

public async Task<IActionResult> Index(string searchString)


{
var movies = from m in _context.Movie
select m;

if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}

return View(await movies.ToListAsync());


}

El método Index actualizado con el parámetro id :


public async Task<IActionResult> Index(string id)
{
var movies = from m in _context.Movie
select m;

if (!String.IsNullOrEmpty(id))
{
movies = movies.Where(s => s.Title.Contains(id));
}

return View(await movies.ToListAsync());


}

Ahora puede pasar el título de la búsqueda como datos de ruta (un segmento de dirección URL ) en lugar de como
un valor de cadena de consulta.

Sin embargo, no se puede esperar que los usuarios modifiquen la dirección URL cada vez que quieran buscar una
película. Por tanto, ahora deberá agregar elementos de la interfaz de usuario con los que podrán filtrar las
películas. Si cambió la firma del método Index para probar cómo pasar el parámetro ID enlazado a una ruta,
vuelva a cambiarlo para que tome un parámetro denominado searchString :

public async Task<IActionResult> Index(string searchString)


{
var movies = from m in _context.Movie
select m;

if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}

return View(await movies.ToListAsync());


}

Abra el archivo Views/Movies/Index.cshtml y agregue el marcado <form> resaltado a continuación:


ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
<a asp-action="Create">Create New</a>
</p>

<form asp-controller="Movies" asp-action="Index">


<p>
Title: <input type="text" name="SearchString">
<input type="submit" value="Filter" />
</p>
</form>

<table class="table">
<thead>

La etiqueta HTML <form> usa la aplicación auxiliar de etiquetas de formulario, por lo que cuando se envía el
formulario, la cadena de filtro se registra en la acción Index del controlador de películas. Guarde los cambios y
después pruebe el filtro.

No hay ninguna sobrecarga [HttpPost] del método Index como cabría esperar. No es necesario, porque el
método no cambia el estado de la aplicación, simplemente filtra los datos.
Después, puede agregar el método [HttpPost] Index siguiente.

[HttpPost]
public string Index(string searchString, bool notUsed)
{
return "From [HttpPost]Index: filter on " + searchString;
}

El parámetro notUsed se usa para crear una sobrecarga para el método Index . Hablaremos sobre esto más
adelante en el tutorial.
Si agrega este método, el invocador de acción coincidiría con el método [HttpPost] Index , mientras que el
método [HttpPost] Index se ejecutaría tal como se muestra en la imagen de abajo.

Sin embargo, aunque agregue esta versión de [HttpPost] al método Index , hay una limitación en cómo se ha
implementado todo esto. Supongamos que quiere marcar una búsqueda en particular o que quiere enviar un
vínculo a sus amigos donde puedan hacer clic para ver la misma lista filtrada de películas. Tenga en cuenta que la
dirección URL de la solicitud HTTP POST es la misma que la dirección URL de la solicitud GET
(localhost:xxxxx/Movies/Index): no hay información de búsqueda en la URL. La información de la cadena de
búsqueda se envía al servidor como un valor de campo de formulario. Puede comprobarlo con las herramientas
de desarrollo del explorador o con la excelente herramienta Fiddler. En la imagen de abajo se muestran las
herramientas de desarrollo del explorador Chrome:
Puede ver el parámetro de búsqueda y el token XSRF en el cuerpo de la solicitud. Tenga en cuenta, como se
mencionó en el tutorial anterior, que la aplicación auxiliar de etiquetas de formulario genera un token XSRF
antifalsificación. Como no se van a modificar datos, no es necesario validar el token con el método del controlador.
El parámetro de búsqueda se encuentra en el cuerpo de solicitud y no en la dirección URL. Por eso no se puede
capturar dicha información para marcarla o compartirla con otros usuarios. Para solucionar este problema,
especificaremos que la solicitud sea HTTP GET .
Cambie la etiqueta <form> en la vista de Razor Views\movie\Index.cshtml para especificar method="get" :
<form asp-controller="Movies" asp-action="Index" method="get">

Ahora, cuando se envía una búsqueda, la URL contiene la cadena de consulta de búsqueda. La búsqueda también
será dirigida al método de acción HttpGet Index , aunque tenga un método HttpPost Index .

El marcado siguiente muestra el cambio en la etiqueta form :

<form asp-controller="Movies" asp-action="Index" method="get">

Agregar búsqueda por género


Agregue la clase MovieGenreViewModel siguiente a la carpeta Models:

using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace MvcMovie.Models
{
public class MovieGenreViewModel
{
public List<Movie> movies;
public SelectList genres;
public string movieGenre { get; set; }
}
}

El modelo de vista de película y género contendrá:


Una lista de películas.
SelectList , que contiene la lista de géneros. Esto permitirá al usuario seleccionar un género de la lista.
movieGenre , que contiene el género seleccionado.

Reemplace el método Index en MoviesController.cs por el código siguiente:


// Requires using Microsoft.AspNetCore.Mvc.Rendering;
public async Task<IActionResult> Index(string movieGenre, string searchString)
{
// Use LINQ to get list of genres.
IQueryable<string> genreQuery = from m in _context.Movie
orderby m.Genre
select m.Genre;

var movies = from m in _context.Movie


select m;

if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}

if (!String.IsNullOrEmpty(movieGenre))
{
movies = movies.Where(x => x.Genre == movieGenre);
}

var movieGenreVM = new MovieGenreViewModel();


movieGenreVM.genres = new SelectList(await genreQuery.Distinct().ToListAsync());
movieGenreVM.movies = await movies.ToListAsync();

return View(movieGenreVM);
}

El código siguiente es una consulta LINQ que recupera todos los géneros de la base de datos.

// Use LINQ to get list of genres.


IQueryable<string> genreQuery = from m in _context.Movie
orderby m.Genre
select m.Genre;

La SelectList de géneros se crea mediante la proyección de los distintos géneros (no queremos que nuestra lista
de selección tenga géneros duplicados).

movieGenreVM.genres = new SelectList(await genreQuery.Distinct().ToListAsync())

Agregar búsqueda por género a la vista de índice


Actualice Index.cshtml de la siguiente manera:
@model MvcMovie.Models.MovieGenreViewModel

@{
ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
<a asp-action="Create">Create New</a>
</p>

<form asp-controller="Movies" asp-action="Index" method="get">


<p>
<select asp-for="movieGenre" asp-items="Model.genres">
<option value="">All</option>
</select>

Title: <input type="text" name="SearchString">


<input type="submit" value="Filter" />
</p>
</form>

<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.movies[0].Title)
</th>
<th>
@Html.DisplayNameFor(model => model.movies[0].ReleaseDate)
</th>
<th>
@Html.DisplayNameFor(model => model.movies[0].Genre)
</th>
<th>
@Html.DisplayNameFor(model => model.movies[0].Price)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.movies)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.ReleaseDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Genre)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
<a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-action="Details" asp-route-id="@item.ID">Details</a> |
<a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
Examine la expresión lambda usada en la siguiente aplicación auxiliar HTML:
@Html.DisplayNameFor(model => model.movies[0].Title)

En el código anterior, la aplicación auxiliar HTML DisplayNameFor inspecciona la propiedad Title a la que se
hace referencia en la expresión lambda para determinar el nombre para mostrar. Puesto que la expresión lambda
se inspecciona en lugar de evaluarse, no recibirá una infracción de acceso cuando model , model.movies o
model.movies[0] sean null o estén vacíos. Cuando se evalúa la expresión lambda (por ejemplo,
@Html.DisplayFor(modelItem => item.Title) ), se evalúan los valores de propiedad del modelo.

Pruebe la aplicación haciendo búsquedas por género, título de la película y ambos.

A N T E R IO R : V IS T A S Y M É T O D O S D E S IG U IE N T E : A G R E G A R U N
C ON TROL A D OR CAM PO
Adición de un nuevo campo
25/06/2018 • 5 minutes to read • Edit Online

Por Rick Anderson


En este tutorial se agregará un nuevo campo a la tabla Movies . Quitaremos la base de datos y crearemos una
nueva cuando cambiemos el esquema (agregar un nuevo campo). Este flujo de trabajo funciona bien al principio
de desarrollo si no tenemos que conservar datos de producción.
Una vez que se haya implementado la aplicación y se tengan datos que se quieran conservar, no se podrá
desconectar la base de datos cuando sea necesario cambiar el esquema. Migraciones de Entity Framework Code
First permite actualizar el esquema y migrar la base de datos sin perder datos. Migraciones es una característica
muy usada con SQL Server, pero SQLite no admite muchas operaciones de esquema de migración, por lo que
solo se pueden realizar migraciones muy sencillas. Para más información, vea SQLite EF Core Database Provider
Limitations (Limitaciones del proveedor de base de datos de SQLite EF Core).

Adición de una propiedad de clasificación al modelo Movie


Abra el archivo Models/Movie.cs y agregue una propiedad Rating :
[!code-csharp]
[!code-csharp]
Dado que ha agregado un nuevo campo a la clase Movie , también debe actualizar la lista blanca de direcciones de
enlace para que incluya esta nueva propiedad. En MoviesController.cs, actualice el atributo [Bind] para que los
métodos de acción Create y Edit incluyan la propiedad Rating :

[Bind("ID,Title,ReleaseDate,Genre,Price,Rating")]

También necesita actualizar las plantillas de vista para mostrar, crear y editar la nueva propiedad Rating en la
vista del explorador.
Edite el archivo /Views/Movies/Index.cshtml y agregue un campo Rating :
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.movies[0].Title)
</th>
<th>
@Html.DisplayNameFor(model => model.movies[0].ReleaseDate)
</th>
<th>
@Html.DisplayNameFor(model => model.movies[0].Genre)
</th>
<th>
@Html.DisplayNameFor(model => model.movies[0].Price)
</th>
<th>
@Html.DisplayNameFor(model => model.movies[0].Rating)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.movies)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.ReleaseDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Genre)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
@Html.DisplayFor(modelItem => item.Rating)
</td>
<td>

Actualice /Views/Movies/Create.cshtml con un campo Rating .


La aplicación no funcionará hasta que la base de datos se actualice para incluir el nuevo campo. Si la ejecuta
ahora, se producirá la siguiente SqliteException :

SqliteException: SQLite Error 1: 'no such column: m.Rating'.

Este error se muestra porque la clase del modelo Movie actualizada es diferente del esquema de la tabla Movie de
la base de datos existente. (No hay ninguna columna Rating en la tabla de la base de datos).
Este error se puede resolver de varias maneras:
1. Desconecte la base de datos y haga que Entity Framework vuelva a crear automáticamente la base de datos
según el nuevo esquema de clase de modelo. Con este enfoque se pierden los datos que tenga en la base
de datos, así que no puede hacer esto con una base de datos de producción. A menudo, una forma
productiva de desarrollar una aplicación consiste en usar un inicializador para propagar una base de datos
con datos de prueba.
2. Modifique manualmente el esquema de la base de datos existente para que coincida con las clases de
modelo. La ventaja de este enfoque es que se conservan los datos. Puede realizar este cambio de forma
manual o mediante la creación de un script de cambio de base de datos.
3. Use Migraciones de Code First para actualizar el esquema de la base de datos.
Para este tutorial, se desconectará la base de datos y se volverá a crear cuando cambie el esquema. Para
desconectar la base de datos, ejecute este comando desde un terminal:
dotnet ef database drop

Actualice la clase SeedData para que proporcione un valor para la nueva columna. A continuación se muestra un
cambio de ejemplo, aunque es conveniente realizarlo con cada new Movie .

new Movie
{
Title = "When Harry Met Sally",
ReleaseDate = DateTime.Parse("1989-1-11"),
Genre = "Romantic Comedy",
Rating = "R",
Price = 7.99M
},

Agregue el campo Rating a la vista Edit , Details y Delete .


Ejecute la aplicación y compruebe que puede crear, editar o mostrar películas con un campo Rating . plantillas.

A N T E R IO R : A G R E G A R S IG U IE N T E : A G R E G A R
BÚSQUEDA V A L ID A C IÓ N
Adición de validación
25/06/2018 • 17 minutes to read • Edit Online

Por Rick Anderson


En esta sección se agregará lógica de validación al modelo Movie y se asegurará de que las reglas de validación
se aplican siempre que un usuario crea o edita una película.

Respetar el principio DRY


Uno de los principios de diseño de MVC es DRY ("Una vez y solo una"). ASP.NET MVC le anima a que especifique
la función o el comportamiento una sola vez y luego lo refleje en el resto de la aplicación. Esto reduce la cantidad
de código que necesita escribir y hace que el código que escribe sea menos propenso a errores, así como más fácil
probar y de mantener.
La compatibilidad de validación proporcionada por MVC y Entity Framework Core Code First es un buen ejemplo
del principio DRY. Puede especificar las reglas de validación mediante declaración en un lugar (en la clase del
modelo) y las reglas se aplican en toda la aplicación.

Adición de reglas de validación al modelo de película


Abra el archivo Movie.cs. DataAnnotations proporciona un conjunto integrado de atributos de validación que se
aplican mediante declaración a cualquier clase o propiedad. (También contiene atributos de formato como
DataType , que ayudan a aplicar formato y no proporcionan validación).

Actualice la clase Movie para aprovechar los atributos de validación integrados Required , StringLength ,
RegularExpression y Range .
[!code-csharp]
[!code-csharp]
Los atributos de validación especifican el comportamiento que quiere aplicar en las propiedades del modelo al
que se aplican. Los atributos Required y MinimumLength indican que una propiedad debe tener un valor, pero nada
evita que un usuario escriba espacios en blanco para satisfacer esta validación. El atributo RegularExpression se
usa para limitar los caracteres que se pueden escribir. En el código anterior, Genre y Rating solamente pueden
usar letras (no se permiten espacios en blanco, números ni caracteres especiales). El atributo Range restringe un
valor a un intervalo determinado. El atributo StringLength permite establecer la longitud máxima de una
propiedad de cadena y, opcionalmente, su longitud mínima. Los tipos de valor (como decimal , int , float ,
DateTime ) son intrínsecamente necesarios y no necesitan el atributo [Required] .

Cuando ASP.NET aplica automáticamente reglas de validación, logramos que la aplicación sea más sólida.
También nos permite asegurarnos de que todo se valida y que no nos dejamos ningún dato incorrecto en la base
de datos accidentalmente.

Interfaz de usuario de error de validación en MVC


Ejecute la aplicación y navegue al controlador Movies.
Pulse el vínculo Crear nueva para agregar una nueva película. Rellene el formulario con algunos valores no
válidos. En cuanto la validación del lado cliente de jQuery detecta el problema, muestra un mensaje de error.
NOTE
Es posible que no pueda escribir comas decimales en el campo Price . Para que la validación de jQuery sea compatible con
configuraciones regionales distintas del inglés que usan una coma (",") en lugar de un punto decimal y formatos de fecha
distintos del de Estados Unidos, debe seguir unos pasos para globalizar la aplicación. Consulte el problema 4076 de GitHub
para obtener instrucciones sobre cómo agregar la coma decimal.

Observe cómo el formulario presenta automáticamente un mensaje de error de validación adecuado en cada
campo que contiene un valor no válido. Los errores se aplican en el lado cliente (con JavaScript y jQuery) y en el
lado servidor (cuando un usuario tiene JavaScript deshabilitado).
Una ventaja importante es que no fue necesario cambiar ni una sola línea de código en la clase MoviesController
o en la vista Create.cshtml para habilitar esta interfaz de usuario de validación. El controlador y las vistas que creó
en pasos anteriores de este tutorial seleccionaron automáticamente las reglas de validación que especificó
mediante atributos de validación en las propiedades de la clase del modelo Movie . Pruebe la aplicación mediante
el método de acción Edit y se aplicará la misma validación.
Los datos del formulario no se enviarán al servidor hasta que dejen de producirse errores de validación de cliente.
Puede comprobarlo colocando un punto de interrupción en el método HTTP Post mediante la herramienta Fiddler
o las herramientas de desarrollo F12.

Cómo funciona la validación


Tal vez se pregunte cómo se generó la validación de la interfaz de usuario sin actualizar el código en el controlador
o las vistas. En el código siguiente se muestran los dos métodos Create .

// GET: Movies/Create
public IActionResult Create()
{
return View();
}

// POST: Movies/Create
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(
[Bind("ID,Title,ReleaseDate,Genre,Price, Rating")] Movie movie)
{
if (ModelState.IsValid)
{
_context.Add(movie);
await _context.SaveChangesAsync();
return RedirectToAction("Index");
}
return View(movie);
}

El primer método de acción Create (HTTP GET) muestra el formulario de creación inicial. La segunda versión (
[HttpPost] ) controla el envío de formulario. El segundo método Create (la versión [HttpPost] ) llama a
ModelState.IsValid para comprobar si la película tiene errores de validación. Al llamar a este método se evalúan
todos los atributos de validación que se hayan aplicado al objeto. Si el objeto tiene errores de validación, el
método Create vuelve a mostrar el formulario. Si no hay ningún error, el método guarda la nueva película en la
base de datos. En nuestro ejemplo de película, el formulario no se publica en el servidor si se detectan errores de
validación del lado cliente; cuando hay errores de validación en el lado cliente, no se llama nunca al segundo
método Create . Si deshabilita JavaScript en el explorador, se deshabilita también la validación del cliente y puede
probar si el método Create HTTP POST ModelState.IsValid detecta errores de validación.
Puede establecer un punto de interrupción en el método [HttpPost] Create y comprobar si nunca se llama al
método. La validación del lado cliente no enviará los datos del formulario si se detectan errores de validación. Si
deshabilita JavaScript en el explorador y después envía el formulario con errores, se alcanzará el punto de
interrupción. Puede seguir obteniendo validación completa sin JavaScript.
En la siguiente imagen se muestra cómo deshabilitar JavaScript en el explorador Firefox.
En la siguiente imagen se muestra cómo deshabilitar JavaScript en el explorador Chrome.

Después de deshabilitar JavaScript, publique los datos no válidos y siga los pasos del depurador.
Abajo se muestra una parte de la plantilla de vista Create.cshtml a la que se aplicó scaffolding en un paso anterior
de este tutorial. Los métodos de acción que se muestran arriba la usan para mostrar el formulario inicial y para
volver a mostrarlo en caso de error.

<form asp-action="Create">
<div class="form-horizontal">
<h4>Movie</h4>
<hr />

<div asp-validation-summary="ModelOnly" class="text-danger"></div>


<div class="form-group">
<label asp-for="Title" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Title" class="form-control" />
<span asp-validation-for="Title" class="text-danger"></span>
</div>
</div>

@*Markup removed for brevity.*@


</div>
</form>

La aplicación auxiliar de etiquetas de entrada usa los atributos DataAnnotations y genera los atributos HTML
necesarios para la validación de jQuery en el lado cliente. La aplicación auxiliar de etiquetas de validación muestra
errores de validación. Para más información, vea Introduction to model validation in ASP.NET Core MVC
(Introducción a la validación de modelos en ASP.NET Core MVC ).
Lo realmente bueno de este enfoque es que ni el controlador ni la plantilla de vista Create saben que las reglas
de validación actuales se están aplicando ni conocen los mensajes de error específicos que se muestran. Las reglas
de validación y las cadenas de error solo se especifican en la clase Movie . Estas mismas reglas de validación se
aplican automáticamente a la vista Edit y a cualquier otra vista de plantillas creada que edite el modelo.
Cuando necesite cambiar la lógica de validación, puede hacerlo exactamente en un solo lugar mediante la adición
de atributos de validación al modelo (en este ejemplo, la clase Movie ). No tendrá que preocuparse de que
diferentes partes de la aplicación sean incoherentes con el modo en que se aplican las reglas: toda la lógica de
validación se definirá en un solo lugar y se usará en todas partes. Esto mantiene el código muy limpio y hace que
sea fácil de mantener y evolucionar. También significa que respeta totalmente el principio DRY.

Uso de atributos DataType


Abra el archivo Movie.cs y examine la clase Movie . El espacio de nombres System.ComponentModel.DataAnnotations
proporciona atributos de formato además del conjunto integrado de atributos de validación. Ya hemos aplicado un
valor de enumeración DataType en la fecha de lanzamiento y los campos de precio. En el código siguiente se
muestran las propiedades ReleaseDate y Price con el atributo DataType adecuado.

[Display(Name = "Release Date")]


[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }

[Range(1, 100)]
[DataType(DataType.Currency)]
public decimal Price { get; set; }

Los atributos DataType solo proporcionan sugerencias para que el motor de vista aplique formato a los datos (y
ofrece atributos o elementos como <a> para las direcciones URL y <a href="mailto:EmailAddress.com"> para el
correo electrónico). Use el atributo RegularExpression para validar el formato de los datos. El atributo DataType
no es un atributo de validación, sino que se usa para especificar un tipo de datos más específico que el tipo
intrínseco de la base de datos. En este caso solo queremos realizar un seguimiento de la fecha, no la hora. La
enumeración DataType proporciona muchos tipos de datos, como Date (Fecha), Time (Hora), PhoneNumber
(Número de teléfono), Currency (Moneda), EmailAddress (Dirección de correo electrónico), etc. El atributo
DataType también puede permitir que la aplicación proporcione automáticamente características específicas del
tipo. Por ejemplo, se puede crear un vínculo mailto: para DataType.EmailAddress y se puede proporcionar un
selector de datos para DataType.Date en exploradores compatibles con HTML5. Los atributos DataType emiten
atributos HTML 5 data- (se pronuncia "datos dash") que los exploradores HTML 5 pueden comprender. Los
atributos DataType no proporcionan ninguna validación.
DataType.Date no especifica el formato de la fecha que se muestra. De manera predeterminada, el campo de
datos se muestra según los formatos predeterminados basados en el elemento CultureInfo del servidor.
El atributo DisplayFormat se usa para especificar el formato de fecha de forma explícita:

[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]


public DateTime ReleaseDate { get; set; }

El valor ApplyFormatInEditMode especifica que el formato se debe aplicar también cuando el valor se muestra en
un cuadro de texto para su edición. En algunos campos este comportamiento puede no ser conveniente. Por poner
un ejemplo, es probable que con valores de moneda no se quiera que el símbolo de la divisa se incluya en el
cuadro de texto editable.
El atributo DisplayFormat puede usarse por sí solo, pero normalmente se recomienda usar el atributo DataType .
El atributo DataType transmite la semántica de los datos en contraposición a cómo se representa en una pantalla
y ofrece las siguientes ventajas que no proporciona DisplayFormat:
El explorador puede habilitar características de HTML5 (por ejemplo, mostrar un control de calendario, el
símbolo de moneda adecuado según la configuración regional, vínculos de correo electrónico, etc.).
De manera predeterminada, el explorador representa los datos con el formato correcto según la
configuración regional.
El atributo DataType puede habilitar MVC para que elija la plantilla de campo adecuada para representar
los datos ( DisplayFormat , si se usa por sí solo, usa la plantilla de cadena).

NOTE
La validación de jQuery no funciona con el atributo Range ni DateTime . Por ejemplo, el código siguiente siempre muestra
un error de validación del lado cliente, incluso cuando la fecha está en el intervalo especificado:
[Range(typeof(DateTime), "1/1/1966", "1/1/2020")]

Debe deshabilitar la validación de fechas de jQuery para usar el atributo Range con DateTime . Por lo general no
se recomienda compilar fechas fijas en los modelos, así que desaconseja usar el atributo Range y DateTime .
El código siguiente muestra la combinación de atributos en una línea:
[!code-csharp]
[!code-csharp]
En la siguiente parte de la serie de tutoriales, revisaremos la aplicación y realizaremos algunas mejoras a los
métodos Details y Delete generados automáticamente.

Recursos adicionales
Trabajar con formularios
Globalización y localización
Introducción a las aplicaciones auxiliares de etiquetas
Creación de aplicaciones auxiliares de etiquetas

A N T E R IO R : A G R E G A R U N S IG U IE N T E : E X A M IN A R L O S M É T O D O S D E T A IL S Y
CAM PO D E L E TE
Examinar los métodos Details y Delete de una
aplicación ASP.NET Core
25/06/2018 • 5 minutes to read • Edit Online

Por Rick Anderson


Abra el controlador Movie y examine el método Details :
[!code-csharp]
[!code-csharp]
El motor de scaffolding de MVC que creó este método de acción agrega un comentario en el que se muestra
una solicitud HTTP que invoca el método. En este caso se trata de una solicitud GET con tres segmentos de
dirección URL, el controlador Movies , el método Details y un valor id . Recuerde que estos segmentos se
definen en Startup.cs.

app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});

EF facilita el proceso de búsqueda de datos mediante el método SingleOrDefaultAsync . Una característica de


seguridad importante integrada en el método es que el código comprueba que el método de búsqueda haya
encontrado una película antes de intentar hacer nada con ella. Por ejemplo, un pirata informático podría
introducir errores en el sitio cambiando la dirección URL creada por los vínculos de
http://localhost:xxxx/Movies/Details/1 a algo parecido a http://localhost:xxxx/Movies/Details/12345 (o algún
otro valor que no represente una película real). Si no comprobara una película null, la aplicación generaría una
excepción.
Examine los métodos Delete y DeleteConfirmed .
[!code-csharp]
[!code-csharp]
Tenga en cuenta que el método HTTP GET Delete no elimina la película especificada, sino que devuelve una vista
de la película donde puede enviar (HttpPost) la eliminación. La acción de efectuar una operación de eliminación
en respuesta a una solicitud GET (o con este propósito efectuar una operación de edición, creación o cualquier
otra operación que modifique los datos) genera una vulnerabilidad de seguridad.
El método [HttpPost] que elimina los datos se denomina DeleteConfirmed para proporcionar al método HTTP
POST una firma o nombre únicos. Las dos firmas de método se muestran a continuación:

// GET: Movies/Delete/5
public async Task<IActionResult> Delete(int? id)
{
// POST: Movies/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{

Common Language Runtime (CLR ) requiere métodos sobrecargados para disponer de una firma de parámetro
única (mismo nombre de método, pero lista de parámetros diferente). En cambio, aquí necesita dos métodos
Delete (uno para GET y otro para POST ) que tienen la misma firma de parámetro (ambos deben aceptar un
número entero como parámetro).
Hay dos enfoques para este problema. Uno consiste en proporcionar nombres diferentes a los métodos, que es
lo que hizo el mecanismo de scaffolding en el ejemplo anterior. Pero esto implica un pequeño problema:
ASP.NET asigna segmentos de una dirección URL a los métodos de acción por nombre y, si cambia el nombre
de un método, normalmente el enrutamiento no podría encontrar ese método. La solución es la que ve en el
ejemplo, que consiste en agregar el atributo ActionName("Delete") al método DeleteConfirmed . Ese atributo
efectúa la asignación para el sistema de enrutamiento para que una dirección URL que incluya /Delete/ para
una solicitud POST busque el método DeleteConfirmed .
Otra solución alternativa común para los métodos que tienen nombres y firmas idénticos consiste en cambiar la
firma del método POST artificialmente para incluir un parámetro adicional (sin usar). Es lo que hicimos en una
publicación anterior, cuando agregamos el parámetro notUsed . Podría hacer lo mismo aquí para el método
[HttpPost] Delete :

// POST: Movies/Delete/6
[ValidateAntiForgeryToken]
public async Task<IActionResult> Delete(int id, bool notUsed)

Publicar en Azure
Vea Publicar una aplicación web de ASP.NET Core en Azure App Service con Visual Studio para obtener
instrucciones sobre cómo publicar esta aplicación en Azure con Visual Studio. También se puede publicar la
aplicación desde la línea de comandos.

A N T E R IO R
Crear una API web con ASP.NET Core y Visual
Studio para Mac
25/06/2018 • 25 minutes to read • Edit Online

Por Rick Anderson y Mike Wasson


En este tutorial se compila una API web para administrar una lista de tareas pendientes. No se construye la
interfaz de usuario.
Hay tres versiones de este tutorial:
macOS: API web con Visual Studio para Mac (este tutorial)
Windows: API web con Visual Studio para Windows
macOS, Linux y Windows: API web con Visual Studio Code

Información general
En este tutorial se crea la siguiente API:

API DESCRIPTION CUERPO DE LA SOLICITUD CUERPO DE LA RESPUESTA

GET /api/todo Obtener todas las tareas Ninguna Matriz de tareas pendientes
pendientes

GET /api/todo/{id} Obtener un elemento por Ninguna Tarea pendiente


identificador

POST /api/todo Agregar un nuevo elemento Tarea pendiente Tarea pendiente

PUT /api/todo/{id} Actualizar un elemento Tarea pendiente Ninguna


existente

DELETE /api/todo/{id} Eliminar un elemento Ninguna Ninguna

El siguiente diagrama muestra el diseño básico de la aplicación.

El cliente es todo lo que consume la API web (aplicación móvil, explorador, etcétera). En este tutorial no se
crea ningún cliente. Postman o curl se utilizan como el cliente para probar la aplicación.
Un modelo es un objeto que representa los datos de la aplicación. En este caso, el único modelo es una
tarea pendiente. Los modelos se representan como clases de C#, también conocidas como clases POCO
(del inglés Plain Old C# Object, objetos CRL antiguos sin formato).
Un controlador es un objeto que controla solicitudes HTTP y crea la respuesta HTTP. Esta aplicación tiene
un único controlador.
Para simplificar el tutorial, la aplicación no usa una base de datos persistente. La aplicación de ejemplo
almacena tareas pendientes en una base de datos en memoria.
Vea Introduction to ASP.NET Core MVC on macOS or Linux (Introducción a ASP.NET Core MVC en macOS o
Linux) para obtener un ejemplo en el que se usa una base de datos persistente.

Requisitos previos
Visual Studio for Mac

Crear el proyecto
En Visual Studio, seleccione Archivo > Nueva solución.

Seleccione Aplicación .NET Core > API web de ASP.NET Core > Siguiente.
Escriba TodoApi en Nombre del proyecto y haga clic en Crear.

Iniciar la aplicación
En Visual Studio, seleccione Ejecutar > Iniciar con depuración para iniciar la aplicación. Visual Studio inicia
un explorador y se desplaza a http://localhost:5000 . Obtendrá un error HTTP 404 (No encontrado). Cambie la
dirección URL a http://localhost:<port>/api/values . Se muestran los datos de ValuesController :

["value1","value2"]
Agregar compatibilidad con Entity Framework Core
Instale el proveedor de base de datos Entity Framework Core InMemory. Este proveedor de base de datos
permite usar Entity Framework Core con una base de datos en memoria.
En el menú Proyecto, seleccione Agregar paquetes NuGet.
Como alternativa, puede hacer clic con el botón derecho en Dependencias y seleccionar Agregar
paquetes.
Escriba EntityFrameworkCore.InMemory en el cuadro de búsqueda.
Seleccione Microsoft.EntityFrameworkCore.InMemory y, luego, Agregar paquete.
Agregar una clase de modelo
Un modelo es un objeto que representa los datos de la aplicación. En este caso, el único modelo es una tarea
pendiente.
En el Explorador de soluciones, haga clic con el botón derecho en el proyecto. Seleccione Agregar > Nueva
carpeta. Asigne a la carpeta el nombre Modelos.

NOTE
Puede colocar clases de modelo en cualquier lugar del proyecto, pero la carpeta Models se usa por convención.

Haga clic con el botón derecho en la carpeta Modelos y seleccione Agregar > Nuevo archivo > General >
Clase vacía. Denomine la clase TodoItem y, después, haga clic en Nuevo.
Reemplace el código generado por el siguiente:

namespace TodoApi.Models
{
public class TodoItem
{
public long Id { get; set; }
public string Name { get; set; }
public bool IsComplete { get; set; }
}
}

La base de datos genera el Id cuando se crea TodoItem .


Crear el contexto de base de datos
El contexto de base de datos es la clase principal que coordina la funcionalidad de Entity Framework para un
modelo de datos determinado. Esta clase se crea derivándola de la clase
Microsoft.EntityFrameworkCore.DbContext .

Agregue una clase TodoContext a la carpeta Modelos.

using Microsoft.EntityFrameworkCore;

namespace TodoApi.Models
{
public class TodoContext : DbContext
{
public TodoContext(DbContextOptions<TodoContext> options)
: base(options)
{
}

public DbSet<TodoItem> TodoItems { get; set; }


}
}

Registrar el contexto de base de datos


En este paso, el contexto de base de datos se registra con el contenedor de inserción de dependencias. Los
servicios (por ejemplo, el contexto de la base de datos) que se registran con el contenedor de inserción de
dependencias (DI) están disponibles para los controladores.
Registre el contexto de la base de datos con el contenedor de servicio mediante la compatibilidad integrada para
inserción de dependencias. Reemplace el contenido del archivo Startup.cs con el código siguiente:
[!code-csharp]
[!code-csharp]
El código anterior:
Quita el código no usado.
Especifica que se inserte una base de datos en memoria en el contenedor de servicios.

Agregar un controlador
En el Explorador de soluciones, en la carpeta Controladores, agregue la clase TodoController .
Reemplace el código generado con el siguiente:
[!code-csharp]
El código anterior define una clase de controlador de API sin métodos. En las secciones siguientes, se agregan
métodos para implementar la API.
[!code-csharp]
El código anterior define una clase de controlador de API sin métodos. En las secciones siguientes, se agregan
métodos para implementar la API. La clase se anota con un atributo [ApiController] para habilitar algunas
características muy prácticas. Para más información sobre las características que el atributo habilita, vea
Anotación de una clase con ApiControllerAttribute.
El constructor del controlador usa la inserción de dependencias para insertar el contexto de base de datos (
TodoContext ) en el controlador. El contexto de base de datos se usa en cada uno de los métodos CRUD del
controlador. El constructor agrega un elemento a la base de datos en memoria si no existe ninguno.

Tareas pendientes
Para obtener tareas pendientes, agregue estos métodos a la clase TodoController :
[!code-csharp]
[!code-csharp]
Estos métodos implementan los dos métodos GET:
GET /api/todo
GET /api/todo/{id}

Esta es una respuesta HTTP de ejemplo del método GetAll :

[
{
"id": 1,
"name": "Item1",
"isComplete": false
}
]

Más adelante en el tutorial, veremos cómo se puede ver la respuesta HTTP por medio de Postman o curl.
Enrutamiento y rutas URL
El atributo [HttpGet] indica un método que responde a una solicitud HTTP GET. La ruta de dirección URL para
cada método se construye como sigue:
Tome la cadena de plantilla en el atributo Route del controlador:
[!code-csharp]
[!code-csharp]
Reemplace [controller] por el nombre del controlador, que es el nombre de clase de controlador sin el
sufijo "Controller". En este ejemplo, el nombre de clase de controlador es TodoController y el nombre de raíz
es "todo". El enrutamiento en ASP.NET Core no distingue entre mayúsculas y minúsculas.
Si el atributo [HttpGet] tiene una plantilla de ruta (como [HttpGet("/products")] ), anexiónela a la ruta de
acceso. En este ejemplo no se usa una plantilla. Para más información, vea Enrutamiento mediante atributos
con atributos Http[Verb].
En el siguiente método GetById , "{id}" es una variable de marcador de posición correspondiente al
identificador único de la tarea pendiente. Cuando GetById se invoca, asigna el valor "{id}" de la dirección URL
al parámetro id del método.
[!code-csharp]
[!code-csharp]
Name = "GetTodo" crea una ruta con nombre. Rutas con nombre:
Permita que la aplicación cree un vínculo HTTP mediante el nombre de ruta.
Se explican más adelante en el tutorial.
Valores devueltos
El método GetAll devuelve una colección de objetos TodoItem . MVC serializa automáticamente el objeto a
JSON y escribe el JSON en el cuerpo del mensaje de respuesta. El código de respuesta para este método es
200, suponiendo que no haya ninguna excepción no controlada. Las excepciones no controladas se convierten
en errores 5xx.
En cambio, el método GetById devuelve el tipo más general IActionResult, que representa una amplia gama de
tipos de valor devuelto. GetById tiene dos tipos de valor devueltos distintos:
Si no hay ningún elemento que coincida con el identificador solicitado, el método devuelve un error 404.
Devolver NotFound genera una respuesta HTTP 404.
En caso contrario, el método devuelve 200 con un cuerpo de respuesta JSON. Devolver Ok genera una
respuesta HTTP 200.
En cambio, el método GetById devuelve el tipo ActionResult<T>, que representa una amplia gama de tipos de
valor devuelto. GetById tiene dos tipos de valor devueltos distintos:
Si no hay ningún elemento que coincida con el identificador solicitado, el método devuelve un error 404.
Devolver NotFound genera una respuesta HTTP 404.
En caso contrario, el método devuelve 200 con un cuerpo de respuesta JSON. Devolver item genera una
respuesta HTTP 200.
Iniciar la aplicación
En Visual Studio, seleccione Ejecutar > Iniciar con depuración para iniciar la aplicación. Visual Studio inicia
un explorador y navega hasta http://localhost:<port> , donde <port> es un número de puerto elegido
aleatoriamente. Obtendrá un error HTTP 404 (No encontrado). Cambie la dirección URL a
http://localhost:<port>/api/values . Se muestran los datos de ValuesController :

["value1","value2"]

Vaya al controlador Todo en http://localhost:<port>/api/todo . Se devuelve el siguiente JSON:

[{"key":1,"name":"Item1","isComplete":false}]

Implementar las otras operaciones CRUD


Vamos a agregar los métodos Create , Update y Delete al controlador. Estos métodos son variaciones de un
tema, así que solo mostraré el código y comentaré las diferencias principales. Compile el proyecto después de
agregar o cambiar el código.
Crear
[!code-csharp]
El código anterior responde a un método HTTP POST, como se puede apreciar por el atributo [HttpPost]. El
atributo [FromBody] indica a MVC que obtenga el valor de la tarea pendiente del cuerpo de la solicitud HTTP.
[!code-csharp]
El código anterior responde a un método HTTP POST, como se puede apreciar por el atributo [HttpPost]. MVC
obtiene el valor de la tarea pendiente del cuerpo de la solicitud HTTP.
El método CreatedAtRoute devuelve una respuesta 201. Se trata de la respuesta estándar de un método HTTP
POST que crea un recurso en el servidor. CreatedAtRoute también agrega un encabezado de ubicación a la
respuesta. El encabezado de ubicación especifica el URI de la tarea pendiente recién creada. Vea 10.2.2 201
Created (10.2.2 201 creada).
Usar Postman para enviar una solicitud de creación
Inicie la aplicación (Ejecutar > Iniciar con depuración).
Abra Postman.

Actualice el número de puerto en la dirección URL de localhost.


Establezca el método HTTP en POST.
Haga clic en la pestaña Body (Cuerpo).
Seleccione el botón de radio Raw (Sin formato).
Establezca el tipo en JSON (application/json).
Escriba un cuerpo de solicitud con una tarea pendiente parecida al siguiente JSON:
{
"name":"walk dog",
"isComplete":true
}

Haga clic en el botón Send (Enviar).

TIP
Si no aparece ninguna respuesta tras hacer clic en Send (Enviar), deshabilite la opción SSL certification verification
(Comprobación de certificación SSL). La encontrará en File > Settings (Archivo > Configuración). Vuelva a hacer clic en el
botón Send (Enviar) después de deshabilitar la configuración.

Haga clic en la pestaña Headers (Encabezados) del panel Response (Respuesta) y copie el valor de encabezado
de Location (Ubicación):

Puede usar el URI del encabezado Location (Ubicación) para tener acceso al recurso que ha creado. El método
Create devuelve CreatedAtRoute. El primer parámetro que se pasa a CreatedAtRoute representa la ruta con
nombre que se usa para generar la dirección URL. Recuerde que el método GetById creó la ruta con nombre
"GetTodo" :

[HttpGet("{id}", Name = "GetTodo")]

Actualizar
[!code-csharp]
[!code-csharp]
Update es similar a Create , pero usa HTTP PUT. La respuesta es 204 Sin contenido. Según la especificación
HTTP, una solicitud PUT requiere que el cliente envíe toda la entidad actualizada, no solo los deltas. Para admitir
actualizaciones parciales, use HTTP PATCH.

{
"key": 1,
"name": "walk dog",
"isComplete": true
}

Eliminar

[HttpDelete("{id}")]
public IActionResult Delete(long id)
{
var todo = _context.TodoItems.Find(id);
if (todo == null)
{
return NotFound();
}

_context.TodoItems.Remove(todo);
_context.SaveChanges();
return NoContent();
}

La respuesta es 204 Sin contenido.


Llamar a Web API con jQuery
En esta sección, se agrega una página HTML que usa jQuery para llamar a Web API. jQuery inicia la solicitud y
actualiza la página con los detalles de la respuesta de la API.
Configure el proyecto para atender archivos estáticos y para permitir la asignación de archivos predeterminada.
Esto se logra invocando los métodos de extensión UseStaticFiles y UseDefaultFiles en Startup.Configure. Para
obtener más información, consulte Archivos estáticos.

public void Configure(IApplicationBuilder app)


{
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseMvc();
}

Agregue un archivo HTML denominado index.html al directorio wwwroot del proyecto. Reemplace el contenido
por el siguiente marcado:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>To-do CRUD</title>
<style>
input[type='submit'], button, [aria-label] {
cursor: pointer;
}
#spoiler {
display: none;
}

table {
font-family: Arial, sans-serif;
border: 1px solid;
border-collapse: collapse;
}

th {
background-color: #0066CC;
color: white;
}

td {
border: 1px solid;
padding: 5px;
}
</style>
</head>
<body>
<h1>To-do CRUD</h1>
<h3>Add</h3>
<form action="javascript:void(0);" method="POST" onsubmit="addItem()">
<input type="text" id="add-name" placeholder="New to-do">
<input type="submit" value="Add">
</form>

<div id="spoiler">
<h3>Edit</h3>
<form class="my-form">
<input type="hidden" id="edit-id">
<input type="checkbox" id="edit-isComplete">
<input type="text" id="edit-name">
<input type="submit" value="Edit">
<a onclick="closeInput()" aria-label="Close">&#10006;</a>
</form>
</div>

<p id="counter"></p>

<table>
<tr>
<th>Is Complete</th>
<th>Name</th>
<th></th>
<th></th>
</tr>
<tbody id="todos"></tbody>
</table>

<script src="https://code.jquery.com/jquery-3.3.1.min.js"
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
crossorigin="anonymous"></script>
<script src="site.js"></script>
</body>
</html>

Agregue un archivo JavaScript denominado site.js al directorio wwwroot del proyecto. Reemplace el contenido
por el siguiente código:

const uri = 'api/todo';


let todos = null;
function getCount(data) {
const el = $('#counter');
let name = 'to-do';
let name = 'to-do';
if (data) {
if (data > 1) {
name = 'to-dos';
}
el.text(data + ' ' + name);
} else {
el.html('No ' + name);
}
}

$(document).ready(function () {
getData();
});

function getData() {
$.ajax({
type: 'GET',
url: uri,
success: function (data) {
$('#todos').empty();
getCount(data.length);
$.each(data, function (key, item) {
const checked = item.isComplete ? 'checked' : '';

$('<tr><td><input disabled="true" type="checkbox" ' + checked + '></td>' +


'<td>' + item.name + '</td>' +
'<td><button onclick="editItem(' + item.id + ')">Edit</button></td>' +
'<td><button onclick="deleteItem(' + item.id + ')">Delete</button></td>' +
'</tr>').appendTo($('#todos'));
});

todos = data;
}
});
}

function addItem() {
const item = {
'name': $('#add-name').val(),
'isComplete': false
};

$.ajax({
type: 'POST',
accepts: 'application/json',
url: uri,
contentType: 'application/json',
data: JSON.stringify(item),
error: function (jqXHR, textStatus, errorThrown) {
alert('here');
},
success: function (result) {
getData();
$('#add-name').val('');
}
});
}

function deleteItem(id) {
$.ajax({
url: uri + '/' + id,
type: 'DELETE',
success: function (result) {
getData();
}
});
}

function editItem(id) {
function editItem(id) {
$.each(todos, function (key, item) {
if (item.id === id) {
$('#edit-name').val(item.name);
$('#edit-id').val(item.id);
$('#edit-isComplete').val(item.isComplete);
}
});
$('#spoiler').css({ 'display': 'block' });
}

$('.my-form').on('submit', function () {
const item = {
'name': $('#edit-name').val(),
'isComplete': $('#edit-isComplete').is(':checked'),
'id': $('#edit-id').val()
};

$.ajax({
url: uri + '/' + $('#edit-id').val(),
type: 'PUT',
accepts: 'application/json',
contentType: 'application/json',
data: JSON.stringify(item),
success: function (result) {
getData();
}
});

closeInput();
return false;
});

function closeInput() {
$('#spoiler').css({ 'display': 'none' });
}

Puede que sea necesario realizar un cambio en la configuración de inicio del proyecto de ASP.NET Core para
comprobar la página HTML localmente. Abra launchSettings.json en el directorio Properties del proyecto. Quite
la propiedad launchUrl para forzar a la aplicación a abrirse en index.html, esto es, el archivo predeterminado del
proyecto.
Existen varias formas de obtener jQuery. En el fragmento de código anterior, la biblioteca se carga desde una red
CDN. Este ejemplo es un ejemplo de CRUD completo de llamada a la API de jQuery. Existen más características
en este ejemplo para hacer que la experiencia sea más completa. Aquí verá algunas explicaciones sobre las
llamadas a la API.
Obtener una lista de tareas pendientes
Para obtener una lista de tareas pendientes, envíe una solicitud HTTP GET a /api/todo.
La función de JQuery ajax envía una solicitud AJAX a la API, que devuelve código JSON que representa un
objeto o una matriz. Esta función puede controlar todas las formas de interacción de HTTP enviando una
solicitud HTTP a la url especificada. GET se emite como type . La función de devolución de llamada success
se invoca si la solicitud se realiza correctamente. En la devolución de llamada, el DOM se actualiza con la
información de la tarea pendiente.
$(document).ready(function () {
getData();
});

function getData() {
$.ajax({
type: 'GET',
url: uri,
success: function (data) {
$('#todos').empty();
getCount(data.length);
$.each(data, function (key, item) {
const checked = item.isComplete ? 'checked' : '';

$('<tr><td><input disabled="true" type="checkbox" ' + checked + '></td>' +


'<td>' + item.name + '</td>' +
'<td><button onclick="editItem(' + item.id + ')">Edit</button></td>' +
'<td><button onclick="deleteItem(' + item.id + ')">Delete</button></td>' +
'</tr>').appendTo($('#todos'));
});

todos = data;
}
});
}

Agregar una tarea pendiente


Para agregar una tarea pendiente, envíe una solicitud HTTP POST a /api/todo. El cuerpo de la solicitud debe
contener un objeto de tarea pendiente. La función ajax usa POST para llamar a la API. En las solicitudes POST y
PUT , el cuerpo de la solicitud representa los datos enviados a la API. La API espera un cuerpo de solicitud con
formato JSON. Las opciones accepts y contentType se establecen en application/json para clasificar el tipo
de medio que se va a recibir y a enviar respectivamente. Los datos se convierten en un objeto JSON usando
JSON.stringify . Cuando la API devuelve un código de estado correcto, se invoca la función getData para
actualizar la tabla HTML.

function addItem() {
const item = {
'name': $('#add-name').val(),
'isComplete': false
};

$.ajax({
type: 'POST',
accepts: 'application/json',
url: uri,
contentType: 'application/json',
data: JSON.stringify(item),
error: function (jqXHR, textStatus, errorThrown) {
alert('here');
},
success: function (result) {
getData();
$('#add-name').val('');
}
});
}

Actualizar una tarea pendiente


Actualizar una tarea pendiente es muy parecido a agregarla, ya que ambos procesos se basan en el cuerpo de
solicitud. En este caso, la única diferencia real entre ambos es que url cambia para reflejar el identificador
único del elemento, y type es PUT .

$.ajax({
url: uri + '/' + $('#edit-id').val(),
type: 'PUT',
accepts: 'application/json',
contentType: 'application/json',
data: JSON.stringify(item),
success: function (result) {
getData();
}
});

Eliminar una tarea pendiente


Para eliminar una tarea pendiente, hay que establecer type de la llamada de AJAX en DELETE y especificar el
identificador único de la tarea en la dirección URL.

$.ajax({
url: uri + '/' + id,
type: 'DELETE',
success: function (result) {
getData();
}
});

Pasos siguientes
Para más información sobre cómo usar una base de datos persistente, vea:
Creación de una aplicación web de páginas de Razor con ASP.NET Core
Trabajo con datos en ASP.NET Core
Páginas de ayuda de ASP.NET Core Web API mediante Swagger
Enrutamiento a acciones del controlador
Compilación de API web con ASP.NET Core
Tipos de valor devuelto de acción del controlador
Para obtener información sobre la implementación de una API, como en Azure App Service, vea la
documentación sobre Hospedaje e implementación.
Vea o descargue el código de ejemplo. Vea cómo descargarlo.
Crear una API web con ASP.NET Core y Visual
Studio Code
17/05/2018 • 25 minutes to read • Edit Online

Por Rick Anderson y Mike Wasson


En este tutorial se compila una API web para administrar una lista de tareas pendientes. No se construye una
interfaz de usuario.
Hay tres versiones de este tutorial:
macOS, Linux y Windows: API Web con Visual Studio Code (este tutorial)
macOS: API Web con Visual Studio para Mac
Windows: API Web con Visual Studio para Windows

Información general
En este tutorial se crea la siguiente API:

API DESCRIPTION CUERPO DE LA SOLICITUD CUERPO DE LA RESPUESTA

GET /api/todo Obtener todas las tareas Ninguna Matriz de tareas pendientes
pendientes

GET /api/todo/{id} Obtener un elemento por Ninguna Tarea pendiente


identificador

POST /api/todo Agregar un nuevo elemento Tarea pendiente Tarea pendiente

PUT /api/todo/{id} Actualizar un elemento Tarea pendiente Ninguna


existente

DELETE /api/todo/{id} Eliminar un elemento Ninguna Ninguna

El siguiente diagrama muestra el diseño básico de la aplicación.

El cliente es todo lo que consume la API web (aplicación móvil, explorador, etcétera). En este tutorial no se
crea ningún cliente. Postman o curl se utilizan como el cliente para probar la aplicación.
Un modelo es un objeto que representa los datos de la aplicación. En este caso, el único modelo es una
tarea pendiente. Los modelos se representan como clases de C#, también conocidas como clases POCO
(del inglés Plain Old C# Object, objetos CRL antiguos sin formato).
Un controlador es un objeto que controla solicitudes HTTP y crea la respuesta HTTP. Esta aplicación tiene
un único controlador.
Para simplificar el tutorial, la aplicación no usa una base de datos persistente. La aplicación de ejemplo
almacena tareas pendientes en una base de datos en memoria.

Requisitos previos
Install the following:
.NET Core SDK 2.0 or later
Visual Studio Code
C# for Visual Studio Code
.NET Core 2.1 SDK or later
Visual Studio Code
C# for Visual Studio Code

Crear el proyecto
Desde una consola, ejecute los siguientes comandos:

dotnet new webapi -o TodoApi


code TodoApi

La carpeta TodoApi se abre en Visual Studio Code (VS Code). Seleccione el archivo Startup.cs.
Seleccione Sí en el mensaje de advertencia "Required assets to build and debug are missing from
'TodoApi'. Add them?" (Faltan los activos necesarios para compilar y depurar en 'TodoApi'. ¿Quiere
agregarlos?).
Seleccione Restaurar en el mensaje de información "There are unresolved dependencies" (Hay
dependencias no resueltas).
Presione Depurar (F5) para compilar y ejecutar el programa. En un navegador, vaya a
http://localhost:5000/api/values. Se muestra el siguiente resultado:

["value1","value2"]

Vea Ayuda de Visual Studio Code para obtener sugerencias sobre el uso de VS Code.

Agregar compatibilidad con Entity Framework Core


Al crear un proyecto en ASP.NET Core 2.0, se agrega la referencia de paquete Microsoft.AspNetCore.All al
archivo TodoApi.csproj:
[!code-xml]
Al crear un proyecto en ASP.NET Core 2.1 o posterior, se agrega la referencia de paquete
Microsoft.AspNetCore.App al archivo TodoApi.csproj:
[!code-xml]
No es necesario instalar el proveedor de base de datos Entity Framework Core InMemory por separado. Este
proveedor de base de datos permite usar Entity Framework Core con una base de datos en memoria.

Agregar una clase de modelo


Un modelo es un objeto que representa los datos de la aplicación. En este caso, el único modelo es una tarea
pendiente.
Agregue una carpeta denominada Models. Puede colocar clases de modelo en cualquier lugar del proyecto, pero
la carpeta Models se usa por convención.
Agregue una clase TodoItem con el siguiente código:
namespace TodoApi.Models
{
public class TodoItem
{
public long Id { get; set; }
public string Name { get; set; }
public bool IsComplete { get; set; }
}
}

La base de datos genera el Id cuando se crea TodoItem .

Crear el contexto de base de datos


El contexto de base de datos es la clase principal que coordina la funcionalidad de Entity Framework para un
modelo de datos determinado. Esta clase se crea al derivar de la clase Microsoft.EntityFrameworkCore.DbContext .
Agregue una clase TodoContext a la carpeta Models:

using Microsoft.EntityFrameworkCore;

namespace TodoApi.Models
{
public class TodoContext : DbContext
{
public TodoContext(DbContextOptions<TodoContext> options)
: base(options)
{
}

public DbSet<TodoItem> TodoItems { get; set; }


}
}

Registrar el contexto de base de datos


En este paso, el contexto de base de datos se registra con el contenedor de inserción de dependencias. Los
servicios (por ejemplo, el contexto de la base de datos) que se registran con el contenedor de inserción de
dependencias (DI) están disponibles para los controladores.
Registre el contexto de la base de datos con el contenedor de servicio mediante la compatibilidad integrada para
inserción de dependencias. Reemplace el contenido del archivo Startup.cs con el código siguiente:
[!code-csharp]
[!code-csharp]
El código anterior:
Quita el código no usado.
Especifica que se inserte una base de datos en memoria en el contenedor de servicios.

Adición de un controlador
En la carpeta Controladores, cree una clase denominada TodoController . Reemplace el contenido por el
siguiente código:
[!code-csharp]
El código anterior define una clase de controlador de API sin métodos. En las secciones siguientes, se agregan
métodos para implementar la API.
[!code-csharp]
El código anterior define una clase de controlador de API sin métodos. En las secciones siguientes, se agregan
métodos para implementar la API. La clase se anota con un atributo [ApiController] para habilitar algunas
características muy prácticas. Para más información sobre las características que el atributo habilita, vea
Anotación de una clase con ApiControllerAttribute.
El constructor del controlador usa la inserción de dependencias para insertar el contexto de base de datos (
TodoContext ) en el controlador. El contexto de base de datos se usa en cada uno de los métodos CRUD del
controlador. El constructor agrega un elemento a la base de datos en memoria si no existe ninguno.

Tareas pendientes
Para obtener tareas pendientes, agregue estos métodos a la clase TodoController :
[!code-csharp]
[!code-csharp]
Estos métodos implementan los dos métodos GET:
GET /api/todo
GET /api/todo/{id}

Esta es una respuesta HTTP de ejemplo del método GetAll :

[
{
"id": 1,
"name": "Item1",
"isComplete": false
}
]

Más adelante en el tutorial, veremos cómo se puede ver la respuesta HTTP por medio de Postman o curl.
Enrutamiento y rutas URL
El atributo [HttpGet] indica un método que responde a una solicitud HTTP GET. La ruta de dirección URL para
cada método se construye como sigue:
Tome la cadena de plantilla en el atributo Route del controlador:
[!code-csharp]
[!code-csharp]
Reemplace [controller] por el nombre del controlador, que es el nombre de clase de controlador sin el
sufijo "Controller". En este ejemplo, el nombre de clase de controlador es TodoController y el nombre de raíz
es "todo". El enrutamiento en ASP.NET Core no distingue entre mayúsculas y minúsculas.
Si el atributo [HttpGet] tiene una plantilla de ruta (como [HttpGet("/products")] ), anexiónela a la ruta de
acceso. En este ejemplo no se usa una plantilla. Para más información, vea Enrutamiento mediante atributos
con atributos Http[Verb].
En el siguiente método GetById , "{id}" es una variable de marcador de posición correspondiente al
identificador único de la tarea pendiente. Cuando GetById se invoca, asigna el valor "{id}" de la dirección
URL al parámetro id del método.
[!code-csharp]
[!code-csharp]
Name = "GetTodo" crea una ruta con nombre. Rutas con nombre:
Permita que la aplicación cree un vínculo HTTP mediante el nombre de ruta.
Se explican más adelante en el tutorial.
Valores devueltos
El método GetAll devuelve una colección de objetos TodoItem . MVC serializa automáticamente el objeto a
JSON y escribe el JSON en el cuerpo del mensaje de respuesta. El código de respuesta para este método es
200, suponiendo que no haya ninguna excepción no controlada. Las excepciones no controladas se convierten
en errores 5xx.
En cambio, el método GetById devuelve el tipo más general IActionResult, que representa una amplia gama de
tipos de valor devuelto. GetById tiene dos tipos de valor devueltos distintos:
Si no hay ningún elemento que coincida con el identificador solicitado, el método devuelve un error 404.
Devolver NotFound genera una respuesta HTTP 404.
En caso contrario, el método devuelve 200 con un cuerpo de respuesta JSON. Devolver Ok genera una
respuesta HTTP 200.
En cambio, el método GetById devuelve el tipo ActionResult<T>, que representa una amplia gama de tipos de
valor devuelto. GetById tiene dos tipos de valor devueltos distintos:
Si no hay ningún elemento que coincida con el identificador solicitado, el método devuelve un error 404.
Devolver NotFound genera una respuesta HTTP 404.
En caso contrario, el método devuelve 200 con un cuerpo de respuesta JSON. Devolver item genera una
respuesta HTTP 200.
Iniciar la aplicación
En VS Code, presione F5 para iniciar la aplicación. Vaya a http://localhost:5000/api/todo (el controlador Todo
que se acaba de crear).

Llamar a Web API con jQuery


En esta sección, se agrega una página HTML que usa jQuery para llamar a Web API. jQuery inicia la solicitud y
actualiza la página con los detalles de la respuesta de la API.
Configure el proyecto para atender archivos estáticos y para permitir la asignación de archivos predeterminada.
Esto se logra invocando los métodos de extensión UseStaticFiles y UseDefaultFiles en Startup.Configure. Para
obtener más información, consulte Archivos estáticos.

public void Configure(IApplicationBuilder app)


{
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseMvc();
}

Agregue un archivo HTML denominado index.html al directorio wwwroot del proyecto. Reemplace el contenido
por el siguiente marcado:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>To-do CRUD</title>
<style>
input[type='submit'], button, [aria-label] {
cursor: pointer;
}

#spoiler {
display: none;
}

table {
font-family: Arial, sans-serif;
border: 1px solid;
border-collapse: collapse;
}

th {
background-color: #0066CC;
color: white;
}

td {
border: 1px solid;
padding: 5px;
}
</style>
</head>
<body>
<h1>To-do CRUD</h1>
<h3>Add</h3>
<form action="javascript:void(0);" method="POST" onsubmit="addItem()">
<input type="text" id="add-name" placeholder="New to-do">
<input type="submit" value="Add">
</form>

<div id="spoiler">
<h3>Edit</h3>
<form class="my-form">
<input type="hidden" id="edit-id">
<input type="checkbox" id="edit-isComplete">
<input type="text" id="edit-name">
<input type="submit" value="Edit">
<a onclick="closeInput()" aria-label="Close">&#10006;</a>
</form>
</div>

<p id="counter"></p>

<table>
<tr>
<th>Is Complete</th>
<th>Name</th>
<th></th>
<th></th>
</tr>
<tbody id="todos"></tbody>
</table>

<script src="https://code.jquery.com/jquery-3.3.1.min.js"
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
crossorigin="anonymous"></script>
<script src="site.js"></script>
</body>
</html>
Agregue un archivo JavaScript denominado site.js al directorio wwwroot del proyecto. Reemplace el contenido
por el siguiente código:

const uri = 'api/todo';


let todos = null;
function getCount(data) {
const el = $('#counter');
let name = 'to-do';
if (data) {
if (data > 1) {
name = 'to-dos';
}
el.text(data + ' ' + name);
} else {
el.html('No ' + name);
}
}

$(document).ready(function () {
getData();
});

function getData() {
$.ajax({
type: 'GET',
url: uri,
success: function (data) {
$('#todos').empty();
getCount(data.length);
$.each(data, function (key, item) {
const checked = item.isComplete ? 'checked' : '';

$('<tr><td><input disabled="true" type="checkbox" ' + checked + '></td>' +


'<td>' + item.name + '</td>' +
'<td><button onclick="editItem(' + item.id + ')">Edit</button></td>' +
'<td><button onclick="deleteItem(' + item.id + ')">Delete</button></td>' +
'</tr>').appendTo($('#todos'));
});

todos = data;
}
});
}

function addItem() {
const item = {
'name': $('#add-name').val(),
'isComplete': false
};

$.ajax({
type: 'POST',
accepts: 'application/json',
url: uri,
contentType: 'application/json',
data: JSON.stringify(item),
error: function (jqXHR, textStatus, errorThrown) {
alert('here');
},
success: function (result) {
getData();
$('#add-name').val('');
}
});
}

function deleteItem(id) {
$.ajax({
$.ajax({
url: uri + '/' + id,
type: 'DELETE',
success: function (result) {
getData();
}
});
}

function editItem(id) {
$.each(todos, function (key, item) {
if (item.id === id) {
$('#edit-name').val(item.name);
$('#edit-id').val(item.id);
$('#edit-isComplete').val(item.isComplete);
}
});
$('#spoiler').css({ 'display': 'block' });
}

$('.my-form').on('submit', function () {
const item = {
'name': $('#edit-name').val(),
'isComplete': $('#edit-isComplete').is(':checked'),
'id': $('#edit-id').val()
};

$.ajax({
url: uri + '/' + $('#edit-id').val(),
type: 'PUT',
accepts: 'application/json',
contentType: 'application/json',
data: JSON.stringify(item),
success: function (result) {
getData();
}
});

closeInput();
return false;
});

function closeInput() {
$('#spoiler').css({ 'display': 'none' });
}

Puede que sea necesario realizar un cambio en la configuración de inicio del proyecto de ASP.NET Core para
comprobar la página HTML localmente. Abra launchSettings.json en el directorio Properties del proyecto. Quite
la propiedad launchUrl para forzar a la aplicación a abrirse en index.html, esto es, el archivo predeterminado
del proyecto.
Existen varias formas de obtener jQuery. En el fragmento de código anterior, la biblioteca se carga desde una
red CDN. Este ejemplo es un ejemplo de CRUD completo de llamada a la API de jQuery. Existen más
características en este ejemplo para hacer que la experiencia sea más completa. Aquí verá algunas explicaciones
sobre las llamadas a la API.
Obtener una lista de tareas pendientes
Para obtener una lista de tareas pendientes, envíe una solicitud HTTP GET a /api/todo.
La función de JQuery ajax envía una solicitud AJAX a la API, que devuelve código JSON que representa un
objeto o una matriz. Esta función puede controlar todas las formas de interacción de HTTP enviando una
solicitud HTTP a la url especificada. GET se emite como type . La función de devolución de llamada success
se invoca si la solicitud se realiza correctamente. En la devolución de llamada, el DOM se actualiza con la
información de la tarea pendiente.
$(document).ready(function () {
getData();
});

function getData() {
$.ajax({
type: 'GET',
url: uri,
success: function (data) {
$('#todos').empty();
getCount(data.length);
$.each(data, function (key, item) {
const checked = item.isComplete ? 'checked' : '';

$('<tr><td><input disabled="true" type="checkbox" ' + checked + '></td>' +


'<td>' + item.name + '</td>' +
'<td><button onclick="editItem(' + item.id + ')">Edit</button></td>' +
'<td><button onclick="deleteItem(' + item.id + ')">Delete</button></td>' +
'</tr>').appendTo($('#todos'));
});

todos = data;
}
});
}

Agregar una tarea pendiente


Para agregar una tarea pendiente, envíe una solicitud HTTP POST a /api/todo. El cuerpo de la solicitud debe
contener un objeto de tarea pendiente. La función ajax usa POST para llamar a la API. En las solicitudes POST y
PUT , el cuerpo de la solicitud representa los datos enviados a la API. La API espera un cuerpo de solicitud con
formato JSON. Las opciones accepts y contentType se establecen en application/json para clasificar el tipo
de medio que se va a recibir y a enviar respectivamente. Los datos se convierten en un objeto JSON usando
JSON.stringify . Cuando la API devuelve un código de estado correcto, se invoca la función getData para
actualizar la tabla HTML.

function addItem() {
const item = {
'name': $('#add-name').val(),
'isComplete': false
};

$.ajax({
type: 'POST',
accepts: 'application/json',
url: uri,
contentType: 'application/json',
data: JSON.stringify(item),
error: function (jqXHR, textStatus, errorThrown) {
alert('here');
},
success: function (result) {
getData();
$('#add-name').val('');
}
});
}

Actualizar una tarea pendiente


Actualizar una tarea pendiente es muy parecido a agregarla, ya que ambos procesos se basan en el cuerpo de
solicitud. En este caso, la única diferencia real entre ambos es que url cambia para reflejar el identificador
único del elemento, y type es PUT .

$.ajax({
url: uri + '/' + $('#edit-id').val(),
type: 'PUT',
accepts: 'application/json',
contentType: 'application/json',
data: JSON.stringify(item),
success: function (result) {
getData();
}
});

Eliminar una tarea pendiente


Para eliminar una tarea pendiente, hay que establecer type de la llamada de AJAX en DELETE y especificar el
identificador único de la tarea en la dirección URL.

$.ajax({
url: uri + '/' + id,
type: 'DELETE',
success: function (result) {
getData();
}
});

Implementar las otras operaciones CRUD


En las secciones siguientes, se agregan los métodos Create , Update y Delete al controlador.
Crear
Agregue el siguiente método Create :
[!code-csharp]
El código anterior es un método HTTP POST, según indica el atributo [HttpPost]. El atributo [FromBody] indica
a MVC que obtenga el valor de la tarea pendiente del cuerpo de la solicitud HTTP.
[!code-csharp]
El código anterior es un método HTTP POST, según indica el atributo [HttpPost]. MVC obtiene el valor de la
tarea pendiente del cuerpo de la solicitud HTTP.
El método CreatedAtRoute realiza las acciones siguientes:
Devuelve una respuesta 201. HTTP 201 es la respuesta estándar para un método HTTP POST que crea un
recurso en el servidor.
Agrega un encabezado de ubicación a la respuesta. El encabezado de ubicación especifica el URI de la tarea
pendiente recién creada. Vea 10.2.2 201 Created (10.2.2 201 creada).
Usa la ruta denominada "GetTodo" para crear la dirección URL. La ruta con nombre "GetTodo" se define en
GetById :

[!code-csharp]
[!code-csharp]
Usar Postman para enviar una solicitud de creación
Inicia la aplicación.
Abra Postman.
Actualice el número de puerto en la dirección URL de localhost.
Establezca el método HTTP en POST.
Haga clic en la pestaña Body (Cuerpo).
Seleccione el botón de radio Raw (Sin formato).
Establezca el tipo en JSON (application/json).
Escriba un cuerpo de solicitud con una tarea pendiente parecida al siguiente JSON:

{
"name":"walk dog",
"isComplete":true
}

Haga clic en el botón Send (Enviar).

TIP
Si no aparece ninguna respuesta tras hacer clic en Send (Enviar), deshabilite la opción SSL certification verification
(Comprobación de certificación SSL). La encontrará en File > Settings (Archivo > Configuración). Vuelva a hacer clic en el
botón Send (Enviar) después de deshabilitar la configuración.

Haga clic en la pestaña Headers (Encabezados) del panel Response (Respuesta) y copie el valor de encabezado
de Location (Ubicación):
El URI del encabezado de ubicación puede utilizarse para acceder al nuevo elemento.
Actualizar
Agregue el siguiente método Update :
[!code-csharp]
[!code-csharp]
Update es similar a Create , salvo por el hecho de que usa HTTP PUT. La respuesta es 204 Sin contenido.
Según la especificación HTTP, una solicitud PUT requiere que el cliente envíe toda la entidad actualizada, no solo
los deltas. Para admitir actualizaciones parciales, use HTTP PATCH.
Use Postman para actualizar el nombre de la tarea pendiente a "walk cat":
Eliminar
Agregue el siguiente método Delete :

[HttpDelete("{id}")]
public IActionResult Delete(long id)
{
var todo = _context.TodoItems.Find(id);
if (todo == null)
{
return NotFound();
}

_context.TodoItems.Remove(todo);
_context.SaveChanges();
return NoContent();
}

La respuesta de Delete es 204 Sin contenido.


Use Postman para eliminar la tarea pendiente:
Ayuda de Visual Studio Code
Introducción
Depuración
Terminal integrado
Métodos abreviados de teclado
Funciones rápidas de teclado de macOS
Métodos abreviados de teclado de Linux
Métodos abreviados de teclado de Windows

Pasos siguientes
Para más información sobre cómo usar una base de datos persistente, vea:
Creación de una aplicación web de páginas de Razor con ASP.NET Core
Trabajo con datos en ASP.NET Core
Páginas de ayuda de ASP.NET Core Web API mediante Swagger
Enrutamiento a acciones del controlador
Compilación de API web con ASP.NET Core
Tipos de valor devuelto de acción del controlador
Para obtener información sobre la implementación de una API, como en Azure App Service, vea la
documentación sobre Hospedaje e implementación.
Vea o descargue el código de ejemplo. Vea cómo descargarlo.
Desarrollar aplicaciones ASP.NET Core con dotnet
watch
14/05/2018 • 4 minutes to read • Edit Online

Por Rick Anderson y Victor Hurdugaci


dotnet watch es una herramienta que ejecuta un comando de la CLI de .NET Core cuando se modifican los
archivos de código fuente. Por ejemplo, un cambio en un archivo puede desencadenar una compilación, una
ejecución de prueba o una implementación.
En este tutorial usaremos una aplicación de API web existente con dos puntos de conexión: uno que devuelva una
suma y otro que devuelva un producto. El método del producto contiene un error que corregiremos en este mismo
tutorial.
Descargue la aplicación de ejemplo. Contiene dos proyectos: WebApp (una API web de ASP.NET Core) y
WebAppTests (pruebas unitarias para la API web).
En un shell de comandos, vaya a la carpeta WebApp y ejecute el siguiente comando:

dotnet run

La salida de la consola muestra mensajes similares al siguiente (indicando que la aplicación se ejecuta y espera
solicitudes):

$ dotnet run
Hosting environment: Development
Content root path: C:/Docs/aspnetcore/tutorials/dotnet-watch/sample/WebApp
Now listening on: http://localhost:5000
Application started. Press Ctrl+C to shut down.

En un explorador web, vaya a http://localhost:<port number>/api/math/sum?a=4&b=5 . Debería ver el resultado de 9


.
Navegue a la API del producto ( http://localhost:<port number>/api/math/product?a=4&b=5 ). Devuelve 9 , no 20 tal
como se esperaría. Lo corregiremos más adelante en el tutorial.

Agregar dotnet watch a un proyecto


1. Agregue una referencia de paquete Microsoft.DotNet.Watcher.Tools al archivo .csproj:

<ItemGroup>
<DotNetCliToolReference Include="Microsoft.DotNet.Watcher.Tools" Version="2.0.0" />
</ItemGroup>

2. Instale el paquete Microsoft.DotNet.Watcher.Tools mediante la ejecución del comando siguiente:

dotnet restore
Ejecución de los comandos de la CLI de .NET Core mediante
dotnet watch
Cualquier comando de la CLI de .NET Core se puede ejecutar con dotnet watch . Por ejemplo:

COMANDO COMANDO CON WATCH

dotnet run dotnet watch run

dotnet run -f netcoreapp2.0 dotnet watch run -f netcoreapp2.0

dotnet run -f netcoreapp2.0 -- --arg1 dotnet watch run -f netcoreapp2.0 -- --arg1

dotnet test dotnet watch test

Ejecute dotnet watch run en la carpeta WebApp. La salida de la consola indica que se ha iniciado watch .

Efectuar cambios con dotnet watch


Asegúrese de que dotnet watch se está ejecutando.
Corrija el error en el método Product de MathController.cs para que devuelva el producto y no la suma:

public static int Product(int a, int b)


{
return a * b;
}

Guarde el archivo. La salida de la consola muestra que dotnet watch ha detectado un cambio de archivo y ha
reiniciado la aplicación.
Compruebe que http://localhost:<port number>/api/math/product?a=4&b=5 devuelve el resultado correcto.

Ejecutar pruebas con dotnet watch


1. Vuelva a cambiar el método Product de MathController.cs para devolver la suma y guarde el archivo.
2. En un shell de comandos, desplácese hasta la carpeta WebAppTests.
3. Ejecute dotnet restore.
4. Ejecute dotnet watch test . La salida que indica que se ha producido un error en una prueba y que el
monitor espera cambios de archivos:

Total tests: 2. Passed: 1. Failed: 1. Skipped: 0.


Test Run Failed.

5. Corrija el código del método Product para que devuelva el producto. Guarde el archivo.

dotnet watch detecta el cambio de archivo y vuelve a ejecutar las pruebas. La salida de la consola indica que se
han superado las pruebas.

dotnet-watch en GitHub
dotnet-watch forma parte del repositorio de DotNetTools de GitHub.
En la sección MSBuild del archivo Léame de dotnet-watch se describe cómo se puede configurar dotnet-watch
desde el archivo de proyecto de MSBuild que se está inspeccionando. El archivo Léame de dotnet-watch contiene
información de dotnet-watch que no se trata en este tutorial.
Crear servicios back-end para aplicaciones móviles
nativas con ASP.NET Core
25/06/2018 • 14 minutes to read • Edit Online

Por Steve Smith


Las aplicaciones móviles pueden comunicarse fácilmente con servicios back-end de ASP.NET Core.
Ver o descargar código de ejemplo de servicios back-end

La aplicación móvil nativa de ejemplo


Este tutorial muestra cómo crear servicios back-end mediante ASP.NET Core MVC para admitir aplicaciones
móviles nativas. Usa la aplicación Xamarin Forms ToDoRest como su cliente nativo, lo que incluye clientes nativos
independientes para dispositivos Android, iOS, Windows Universal y Windows Phone. Puede seguir el tutorial
vinculado para crear la aplicación nativa (e instalar las herramientas de Xamarin gratuitas necesarias), así como
descargar la solución de ejemplo de Xamarin. El ejemplo de Xamarin incluye un proyecto de servicios de ASP.NET
Web API 2, que sustituye a las aplicaciones de ASP.NET Core de este artículo (sin cambios requeridos por el
cliente).
Características
La aplicación ToDoRest permite enumerar, agregar, eliminar y actualizar tareas pendientes. Cada tarea tiene un
identificador, un nombre, notas y una propiedad que indica si ya se ha realizado.
La vista principal de las tareas, como se muestra anteriormente, indica el nombre de cada tarea e indica si se ha
realizado con una marca de verificación.
Al pulsar el icono + se abre un cuadro de diálogo para agregar un elemento:

Al pulsar un elemento en la pantalla de la lista principal se abre un cuadro de diálogo de edición, donde se puede
modificar el nombre del elemento, las notas y la configuración de Done (Listo), o se puede eliminar el elemento:
Este ejemplo está configurado para usar de forma predeterminada servicios back-end hospedados en
developer.xamarin.com, lo que permite operaciones de solo lectura. Para probarlo usted mismo con la aplicación de
ASP.NET Core que creó en la siguiente sección que se ejecuta en el equipo, debe actualizar la constante RestUrl de
la aplicación. Vaya hasta el proyecto ToDoREST y abra el archivo Constants.cs. Reemplace RestUrl con una
dirección URL que incluya la dirección IP de su equipo (no localhost ni 127.0.0.1, puesto que esta dirección se usa
desde el emulador de dispositivo, no desde el equipo). Incluya también el número de puerto (5000). Para
comprobar que los servicios funcionan con un dispositivo, asegúrese de que no tiene un firewall activo que bloquea
el acceso a este puerto.

// URL of REST service (Xamarin ReadOnly Service)


//public static string RestUrl = "http://developer.xamarin.com:8081/api/todoitems{0}";

// use your machine's IP address


public static string RestUrl = "http://192.168.1.207:5000/api/todoitems/{0}";

Creación del proyecto de ASP.NET Core


Cree una aplicación web de ASP.NET Core en Visual Studio. Elija la plantilla de API web y Sin autenticación.
Denomine el proyecto ToDoApi.
La aplicación debería responder a todas las solicitudes realizadas al puerto 5000. Actualice Program.cs para que
incluya .UseUrls("http://*:5000") para conseguir lo siguiente:

var host = new WebHostBuilder()


.UseKestrel()
.UseUrls("http://*:5000")
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<Startup>()
.Build();

NOTE
Asegúrese de que ejecuta la aplicación directamente, en lugar de tras IIS Express, que omite las solicitudes no locales de forma
predeterminada. Ejecute dotnet run desde un símbolo del sistema o elija el perfil del nombre de aplicación en la lista
desplegable de destino de depuración en la barra de herramientas de Visual Studio.

Agregue una clase de modelo para representar las tareas pendientes. Marque los campos obligatorios mediante el
atributo [Required] :
using System.ComponentModel.DataAnnotations;

namespace ToDoApi.Models
{
public class ToDoItem
{
[Required]
public string ID { get; set; }

[Required]
public string Name { get; set; }

[Required]
public string Notes { get; set; }

public bool Done { get; set; }


}
}

Los métodos de API necesitan alguna manera de trabajar con los datos. Use la misma interfaz de IToDoRepository
que usa el ejemplo original de Xamarin:

using System.Collections.Generic;
using ToDoApi.Models;

namespace ToDoApi.Interfaces
{
public interface IToDoRepository
{
bool DoesItemExist(string id);
IEnumerable<ToDoItem> All { get; }
ToDoItem Find(string id);
void Insert(ToDoItem item);
void Update(ToDoItem item);
void Delete(string id);
}
}

En este ejemplo, la implementación usa solo una colección de elementos privada:

using System.Collections.Generic;
using System.Linq;
using ToDoApi.Interfaces;
using ToDoApi.Models;

namespace ToDoApi.Services
{
public class ToDoRepository : IToDoRepository
{
private List<ToDoItem> _toDoList;

public ToDoRepository()
{
InitializeData();
}

public IEnumerable<ToDoItem> All


{
get { return _toDoList; }
}

public bool DoesItemExist(string id)


{
return _toDoList.Any(item => item.ID == id);
return _toDoList.Any(item => item.ID == id);
}

public ToDoItem Find(string id)


{
return _toDoList.FirstOrDefault(item => item.ID == id);
}

public void Insert(ToDoItem item)


{
_toDoList.Add(item);
}

public void Update(ToDoItem item)


{
var todoItem = this.Find(item.ID);
var index = _toDoList.IndexOf(todoItem);
_toDoList.RemoveAt(index);
_toDoList.Insert(index, item);
}

public void Delete(string id)


{
_toDoList.Remove(this.Find(id));
}

private void InitializeData()


{
_toDoList = new List<ToDoItem>();

var todoItem1 = new ToDoItem


{
ID = "6bb8a868-dba1-4f1a-93b7-24ebce87e243",
Name = "Learn app development",
Notes = "Attend Xamarin University",
Done = true
};

var todoItem2 = new ToDoItem


{
ID = "b94afb54-a1cb-4313-8af3-b7511551b33b",
Name = "Develop apps",
Notes = "Use Xamarin Studio/Visual Studio",
Done = false
};

var todoItem3 = new ToDoItem


{
ID = "ecfa6f80-3671-4911-aabe-63cc442c1ecf",
Name = "Publish apps",
Notes = "All app stores",
Done = false,
};

_toDoList.Add(todoItem1);
_toDoList.Add(todoItem2);
_toDoList.Add(todoItem3);
}
}
}

Configure la implementación en Startup.cs:


public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddMvc();

services.AddSingleton<IToDoRepository,ToDoRepository>();
}

En este punto, está listo para crear el ToDoItemsController.

TIP
Obtenga más información sobre cómo crear API web en Cree su primera API web con ASP.NET Core MVC y Visual Studio.

Crear el controlador
Agregue un nuevo controlador para el proyecto: ToDoItemsController. Debe heredar de
Microsoft.AspNetCore.Mvc.Controller. Agregue un atributo Route para indicar que el controlador controlará las
solicitudes realizadas a las rutas de acceso que comiencen con api/todoitems . El token [controller] de la ruta se
sustituye por el nombre del controlador (si se omite el sufijo Controller ) y es especialmente útil para las rutas
globales. Obtenga más información sobre el enrutamiento.
El controlador necesita un IToDoRepository para funcionar. Solicite una instancia de este tipo a través del
constructor del controlador. En tiempo de ejecución, esta instancia se proporcionará con la compatibilidad del
marco con la inserción de dependencias.

using System;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using ToDoApi.Interfaces;
using ToDoApi.Models;

namespace ToDoApi.Controllers
{
[Route("api/[controller]")]
public class ToDoItemsController : Controller
{
private readonly IToDoRepository _toDoRepository;

public ToDoItemsController(IToDoRepository toDoRepository)


{
_toDoRepository = toDoRepository;
}

Esta API es compatible con cuatro verbos HTTP diferentes para realizar operaciones CRUD (creación, lectura,
actualización, eliminación) en el origen de datos. La más simple de ellas es la operación de lectura, que corresponde
a una solicitud HTTP GET.
Leer elementos
La solicitud de una lista de elementos se realiza con una solicitud GET al método List . El atributo [HttpGet] en el
método List indica que esta acción solo debe controlar las solicitudes GET. La ruta de esta acción es la ruta
especificada en el controlador. No es necesario usar el nombre de acción como parte de la ruta. Solo debe
asegurarse de que cada acción tiene una ruta única e inequívoca. El enrutamiento de atributos se puede aplicar
tanto a los niveles de controlador como de método para crear rutas específicas.
[HttpGet]
public IActionResult List()
{
return Ok(_toDoRepository.All);
}

El método List devuelve un código de respuesta 200 OK y todos los elementos de lista de tareas, serializados
como JSON.
Puede probar el nuevo método de API con una variedad de herramientas, como Postman, que se muestra a
continuación:

Crear elementos
Por convención, la creación de elementos de datos se asigna al verbo HTTP POST. El método Create tiene un
atributo [HttpPost] aplicado y acepta una instancia ToDoItem . Puesto que el argumento item se pasará en el
cuerpo de la solicitud POST, este parámetro se decora con el atributo [FromBody] .
Dentro del método, se comprueba la validez del elemento y si existió anteriormente en el almacén de datos y, si no
hay problemas, se agrega mediante el repositorio. Al comprobar ModelState.IsValid se realiza una validación de
modelos, y debe realizarse en cada método de API que acepte datos proporcionados por usuario.
[HttpPost]
public IActionResult Create([FromBody] ToDoItem item)
{
try
{
if (item == null || !ModelState.IsValid)
{
return BadRequest(ErrorCode.TodoItemNameAndNotesRequired.ToString());
}
bool itemExists = _toDoRepository.DoesItemExist(item.ID);
if (itemExists)
{
return StatusCode(StatusCodes.Status409Conflict, ErrorCode.TodoItemIDInUse.ToString());
}
_toDoRepository.Insert(item);
}
catch (Exception)
{
return BadRequest(ErrorCode.CouldNotCreateItem.ToString());
}
return Ok(item);
}

El ejemplo usa una enumeración que contiene códigos de error que se pasan al cliente móvil:

public enum ErrorCode


{
TodoItemNameAndNotesRequired,
TodoItemIDInUse,
RecordNotFound,
CouldNotCreateItem,
CouldNotUpdateItem,
CouldNotDeleteItem
}

Pruebe a agregar nuevos elementos con Postman eligiendo el verbo POST que proporciona el nuevo objeto en
formato JSON en el cuerpo de la solicitud. También debe agregar un encabezado de solicitud que especifica un
Content-Type de application/json .
El método devuelve el elemento recién creado en la respuesta.
Actualizar elementos
La modificación de registros se realiza mediante solicitudes HTTP PUT. Aparte de este cambio, el método Edit es
casi idéntico a Create . Tenga en cuenta que, si no se encuentra el registro, la acción Edit devolverá una respuesta
NotFound (404 ).
[HttpPut]
public IActionResult Edit([FromBody] ToDoItem item)
{
try
{
if (item == null || !ModelState.IsValid)
{
return BadRequest(ErrorCode.TodoItemNameAndNotesRequired.ToString());
}
var existingItem = _toDoRepository.Find(item.ID);
if (existingItem == null)
{
return NotFound(ErrorCode.RecordNotFound.ToString());
}
_toDoRepository.Update(item);
}
catch (Exception)
{
return BadRequest(ErrorCode.CouldNotUpdateItem.ToString());
}
return NoContent();
}

Para probar con Postman, cambie el verbo a PUT. Especifique los datos actualizados del objeto en el cuerpo de la
solicitud.

Este método devuelve una respuesta NoContent (204) cuando se realiza correctamente, para mantener la
coherencia con la API existente.
Eliminar elementos
La eliminación de registros se consigue mediante solicitudes DELETE al servicio y pasando el identificador del
elemento que va a eliminar. Al igual que con las actualizaciones, las solicitudes para elementos que no existen
recibirán respuestas NotFound . De lo contrario, las solicitudes correctas recibirán una respuesta NoContent (204).

[HttpDelete("{id}")]
public IActionResult Delete(string id)
{
try
{
var item = _toDoRepository.Find(id);
if (item == null)
{
return NotFound(ErrorCode.RecordNotFound.ToString());
}
_toDoRepository.Delete(id);
}
catch (Exception)
{
return BadRequest(ErrorCode.CouldNotDeleteItem.ToString());
}
return NoContent();
}

Tenga en cuenta que, al probar la funcionalidad de eliminar, no se necesita nada en el cuerpo de la solicitud.

Convenciones comunes de Web API


Al desarrollar los servicios back-end de la aplicación, necesitará acceder a un conjunto coherente de convenciones o
directivas para controlar cuestiones transversales. Por ejemplo, en el servicio mostrado anteriormente, las
solicitudes de registros específicos que no se encontraron recibieron una respuesta NotFound , en lugar de una
respuesta BadRequest . De forma similar, los comandos realizados a este servicio que pasaron en tipos enlazados a
un modelo siempre se comprobaron como ModelState.IsValid y devolvieron una BadRequest para los tipos de
modelos no válidos.
Después de identificar una directiva común para las API, normalmente puede encapsularla en un filtro. Obtenga
más información sobre cómo encapsular directivas de API comunes en aplicaciones de ASP.NET Core MVC.
Conceptos básicos de ASP.NET Core
19/06/2018 • 14 minutes to read • Edit Online

Una aplicación de ASP.NET Core es una aplicación de consola que crea un servidor web en su método Main :
ASP.NET Core 2.x
ASP.NET Core 1.x

using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;

namespace aspnetcoreapp
{
public class Program
{
public static void Main(string[] args)
{
BuildWebHost(args).Run();
}

public static IWebHost BuildWebHost(string[] args) =>


WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.Build();
}
}

El método Main invoca a WebHost.CreateDefaultBuilder , que sigue el patrón de generador para crear un host de
aplicación web. El generador tiene métodos que definen el servidor web (por ejemplo, UseKestrel ) y la clase de
inicio ( UseStartup ). En el ejemplo anterior, se asigna automáticamente el servidor web Kestrel. El host web de
ASP.NET Core intenta ejecutarse en IIS, si está disponible. Otros servidores web, como HTTP.sys, se pueden usar al
invocar el método de extensión adecuado. UseStartup se explica en la sección siguiente.
IWebHostBuilder, el tipo de valor devuelto de la invocación WebHost.CreateDefaultBuilder , proporciona muchos
métodos opcionales. Algunos de estos métodos incluyen UseHttpSys para hospedar la aplicación en HTTP.sys y
UseContentRoot para especificar el directorio de contenido raíz. Los métodos Build y Run crean el objeto
IWebHost que hospeda la aplicación y empieza a escuchar las solicitudes HTTP.

Inicio
El método UseStartup de WebHostBuilder especifica la clase Startup para la aplicación:
ASP.NET Core 2.x
ASP.NET Core 1.x
public class Program
{
public static void Main(string[] args)
{
BuildWebHost(args).Run();
}

public static IWebHost BuildWebHost(string[] args) =>


WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.Build();
}

La clase Startup es donde se define la canalización de control de solicitudes y donde se configuran los servicios
necesarios para la aplicación. La clase Startup debe ser pública y contener los siguientes métodos:

public class Startup


{
// This method gets called by the runtime. Use this method
// to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
}

// This method gets called by the runtime. Use this method


// to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app)
{
}
}

ConfigureServices define los servicios que usa la aplicación (por ejemplo, ASP.NET Core MVC, Entity Framework
Core, Identity). Configure define el software intermedio en la canalización de solicitudes.
Para obtener más información, vea Application startup (Inicio de la aplicación).

Raíz del contenido


La raíz del contenido es la ruta de acceso base a cualquier contenido que usa la aplicación, como vistas, páginas de
Razor y activos estáticos. De forma predeterminada, la raíz del contenido es la misma que la ruta de acceso base de
la aplicación para el archivo ejecutable que hospeda la aplicación.

Raíz web
La raíz web de una aplicación es el directorio del proyecto que contiene recursos públicos y estáticos, como
archivos de imagen, CSS y JavaScript.

Inserción de dependencias (servicios)


Un servicio es un componente que está pensado para su uso común en una aplicación. Los servicios se ponen a
disposición a través de la inserción de dependencias (DI). ASP.NET Core incluye un contenedor de inversión del
control (IoC ) nativo que admite la inserción de constructores de forma predeterminada. Si quiere, puede
reemplazar el contenedor nativo predeterminado. Además de la ventaja de acoplamiento flexible, DI hace que los
servicios estén disponibles en toda la aplicación (por ejemplo, el registro).
Para obtener más información, consulte Inserción de dependencias.
Software intermedio
En ASP.NET Core, se crea la canalización de solicitudes mediante software intermedio. El software intermedio de
ASP.NET Core lleva a cabo la lógica asincrónica en HttpContext y después invoca al siguiente software intermedio
de la secuencia o finaliza la solicitud directamente. Se agrega un componente de software intermedio denominado
"XYZ" al invocar un método de extensión UseXYZ en el método Configure .
ASP.NET Core incluye un amplio conjunto de middleware integrado:
Archivos estáticos
Enrutamiento
Autenticación
Middleware de compresión de respuestas
Middleware de reescritura de dirección URL
El software intermedio basado en OWIN está disponible para aplicaciones ASP.NET Core y puede escribir su
propio software intermedio personalizado.
Para obtener más información, consulte Middleware (Software intermedio) y Open Web Interface for .NET
(OWIN ) [Interfaz web abierta para .NET (OWIN )].

Inicio de solicitudes HTTP


Para obtener información sobre el uso de IHttpClientFactory para acceder a instancias HttpClient a fin de
realizar solicitudes HTTP, vea Inicio de solicitudes HTTP.

Entornos
Los entornos, como "Desarrollo" y "Producción", son un concepto de primera clase en ASP.NET Core y se pueden
establecer mediante variables de entorno.
Para obtener más información, consulte Uso de varios entornos.

Configuración
ASP.NET Core usa un modelo de configuración basado en pares de nombre-valor. El modelo de configuración no
se basa en System.Configuration o web.config. La configuración obtiene valores de un conjunto ordenado de
proveedores de configuración. Los proveedores de configuración integrados admiten una variedad de formatos de
archivo (XML, JSON, INI) y variables de entorno para habilitar la configuración basada en el entorno. También
puede escribir sus propios proveedores de configuración personalizados.
Para obtener más información, vea Configuración.

Registro
ASP.NET Core es compatible con una API de registro que funciona con una variedad de proveedores de registro.
Los proveedores integrados admiten el envío de registros a uno o varios destinos. Se pueden usar plataformas de
registro de terceros.
Registro

Control de errores
ASP.NET Core tiene características integradas para controlar los errores en las aplicaciones, incluida una página de
excepciones de desarrollador, páginas de errores personalizados, páginas de códigos de estado estáticos y control
de excepciones de inicio.
Para más información, vea Introducción al control de errores.

Enrutamiento
ASP.NET Core ofrece características para el enrutamiento de solicitudes de aplicación a los controladores de ruta.
Para más información, vea Enrutamiento.

Proveedores de archivos
ASP.NET Core abstrae el acceso al sistema de archivos mediante el uso de proveedores de archivos, lo que ofrece
una interfaz común para trabajar con archivos entre plataformas.
Para más información, vea Proveedores de archivos.

Archivos estáticos
El software intermedio de archivos estáticos trabaja con archivos estáticos (por ejemplo, HTML, CSS, imágenes y
JavaScript).
Para obtener más información, consulte Archivos estáticos.

Hospedaje
Las aplicaciones ASP.NET Core configuran e inician un host. Dicho host es responsable de la administración de
inicio y duración de la aplicación.
Para obtener más información, consulte Hospedaje en ASP.NET Core.

Estado de sesión y aplicación


El estado de sesión es una característica de ASP.NET Core que se puede usar para guardar y almacenar datos de
usuario mientras el usuario explora la aplicación web.
Para más información, vea Estado de sesión y aplicación.

Servidores
El modelo de hospedaje de ASP.NET Core no escucha directamente las solicitudes. El modelo de hospedaje se basa
en una implementación de servidor HTTP para reenviar la solicitud a la aplicación. La solicitud reenviada se
empaqueta como un conjunto de objetos de característica al que se puede tener acceso a través de interfaces.
ASP.NET Core incluye un servidor web administrado multiplataforma, denominado Kestrel. Kestrel se suele
ejecutar detrás de un servidor web de producción como IIS o Nginx. Kestrel se puede ejecutar como un servidor
perimetral.
Para más información, vea Servidores y los temas siguientes:
Kestrel
Módulo ASP.NET Core
HTTP.sys (anteriormente denominado WebListener)

Globalización y localización
El hecho de crear un sitio web multilingüe con ASP.NET Core permite que este llegue a un público más amplio.
ASP.NET Core proporciona servicios y software intermedio para la localización en diferentes idiomas y referencias
culturales.
Para más información, vea Globalización y localización.

Características de la solicitud
Los detalles de implementación del servidor web relacionados con las solicitudes HTTP y las respuestas se definen
en las interfaces. En las implementaciones del servidor web y el software intermedio se usan estas interfaces para
crear y modificar la canalización de hospedaje de la aplicación.
Para más información, vea Características de la solicitud.

Tareas en segundo plano


Las tareas en segundo plano se implementan como servicios hospedados. Un servicio hospedado es una clase con
lógica de tarea en segundo plano que implementa la interfaz IHostedService.
Para más información, consulte Tareas en segundo plano con servicios hospedados.

Interfaz web abierta para .NET (OWIN)


ASP.NET Core es compatible con la interfaz web abierta para .NET (OWIN ). OWIN permite que las aplicaciones
web se desacoplen de los servidores web.
Para más información, vea Interfaz web abierta para .NET (OWIN ).

WebSockets
WebSocket es un protocolo que habilita canales de comunicación bidireccional persistentes a través de conexiones
TCP. Se usa para aplicaciones de chat, tableros de cotizaciones, juegos y donde se necesite funcionalidad en tiempo
real en una aplicación web. ASP.NET Core es compatible con características de socket web.
Para más información, vea WebSockets.

Metapaquete Microsoft.AspNetCore.All
El metapaquete Microsoft.AspNetCore.All para ASP.NET Core incluye lo siguiente:
Todos los paquetes admitidos por el equipo de ASP.NET Core.
Todos los paquetes admitidos por Entity Framework Core.
Dependencias internas y de terceros usadas por ASP.NET Core y Entity Framework Core.
Para más información, vea Metapaquete Microsoft.AspNetCore.All.

Entorno de ejecución de .NET Core frente a .NET Framework


Una aplicación de ASP.NET Core puede tener como destino el entorno de ejecución de .NET Core o .NET
Framework.
Para más información, vea Selección entre .NET Core y .NET Framework.

Elección entre ASP.NET Core y ASP.NET


Para más información sobre cómo elegir entre ASP.NET Core y ASP.NET, vea Elección entre ASP.NET Core y
ASP.NET.
Inicio de la aplicación en ASP.NET Core
18/06/2018 • 15 minutes to read • Edit Online

Por Steve Smith, Tom Dykstra y Luke Latham


La clase Startup configura los servicios y la canalización de solicitudes de la aplicación.

Clase Startup
Las aplicaciones de ASP.NET Core utilizan una clase Startup , que se denomina Startup por convención. La
clase Startup :
Puede incluir opcionalmente un método ConfigureServices para configurar los servicios de la aplicación.
Debe incluir un método Configure para crear la canalización de procesamiento de solicitudes de la
aplicación.
El runtime llama a ConfigureServices y Configure al iniciarse la aplicación:

public class Startup


{
// Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
...
}

// Use this method to configure the HTTP request pipeline.


public void Configure(IApplicationBuilder app)
{
...
}
}

Especifique la clase Startup con el método WebHostBuilderExtensions UseStartup<TStartup>:

public class Program


{
public static void Main(string[] args)
{
BuildWebHost(args).Run();
}

public static IWebHost BuildWebHost(string[] args) =>


WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.Build();
}

El constructor de clase Startup acepta dependencias definidas por el host. Un uso común de la inserción de
dependencias en la clase Startup consiste en insertar:
IHostingEnvironment para configurar servicios según el entorno;
IConfiguration para configurar la aplicación durante el inicio.
public class Startup
{
public Startup(IHostingEnvironment env, IConfiguration config)
{
HostingEnvironment = env;
Configuration = config;
}

public IHostingEnvironment HostingEnvironment { get; }


public IConfiguration Configuration { get; }

public void ConfigureServices(IServiceCollection services)


{
if (HostingEnvironment.IsDevelopment())
{
// Development configuration
}
else
{
// Staging/Production configuration
}

// Configuration is available during startup. Examples:


// Configuration["key"]
// Configuration["subsection:suboption1"]
}
}

Una alternativa a la inserción de IHostingEnvironment consiste en utilizar un enfoque basado en


convenciones. La aplicación puede definir clases Startup independientes para los distintos entornos (por
ejemplo, StartupDevelopment ), mientras que la clase de inicio correspondiente se selecciona en tiempo de
ejecución. La clase cuyo sufijo de nombre coincide con el entorno actual se establece como prioritaria. Si la
aplicación se ejecuta en el entorno de desarrollo e incluye tanto la clase Startup como la clase
StartupDevelopment , se utiliza la clase StartupDevelopment . Para obtener más información, consulte Uso de
varios entornos.
Para obtener más información sobre WebHostBuilder , consulte el tema Hospedaje. Para obtener información
sobre cómo controlar los errores que se producen durante el inicio, consulte Control de excepciones de
inicio.

Método ConfigureServices
Características del método ConfigureServices:
Optional
Lo llama el host de web antes del método Configure para configurar los servicios de la aplicación.
Es donde se establecen por convención las opciones de configuración.
La adición de servicios al contenedor de servicios hace que estén disponibles en la aplicación y en el método
Configure . Los servicios se resuelven mediante inserción de dependencias o desde
IApplicationBuilder.ApplicationServices.
El host de web puede configurar algunos servicios antes de que se llame a los métodos Startup . Los
detalles están disponibles en el tema Hospedaje en ASP.NET Core.
Para las características que requieren una configuración sustancial, hay métodos de extensión Add[Service]
en IServiceCollection. Una aplicación web típica registra los servicios de Entity Framework, Identity y MVC:
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();

services.AddMvc();

// Add application services.


services.AddTransient<IEmailSender, AuthMessageSender>();
services.AddTransient<ISmsSender, AuthMessageSender>();
}

SetCompatibilityVersion en ASP.NET Core MVC


El método SetCompatibilityVersion permite a una aplicación participar o no en los cambios de
comportamiento importantes incorporados en ASP.NET MVC Core 2.1+. Estos cambios de comportamiento
importantes suelen estar relacionados con cómo se comporta el subsistema de MVC y cómo el tiempo de
ejecución llama al código. Si la aplicación participa, obtendrá el comportamiento más reciente y a largo
plazo de ASP.NET Core.
El siguiente código establece el modo de compatibilidad en ASP.NET Core 2.1:
[!code-csharpMain]
Le recomendamos que pruebe la aplicación con la versión más reciente ( CompatibilityVersion.Version_2_1 ).
Prevemos que, en la mayoría de las aplicaciones, los cambios de comportamiento importantes no van a usar
la versión más reciente.
Las aplicaciones que llaman a SetCompatibilityVersion(CompatibilityVersion.Version_2_0) están protegidas
frente a los cambios de comportamiento importantes incorporados en ASP.NET Core 2.1 MVC y versiones
2.x posteriores. Esta protección:
No es aplicable a todos los cambios de 2.1 y versiones posteriores, sino que tiene como destino los
cambios importantes de comportamiento en tiempo de ejecución de ASP.NET Core en el subsistema de
MVC.
No se extiende a la siguiente versión principal.
La compatibilidad predeterminada de las aplicaciones ASP.NET Core 2.1 y versiones 2.x posteriores que no
llaman a SetCompatibilityVersion es la compatibilidad 2.0. Es decir, no llamar a SetCompatibilityVersion es
igual que llamar a SetCompatibilityVersion(CompatibilityVersion.Version_2_0) .
El siguiente código establece el modo de compatibilidad en ASP.NET Core 2.1, salvo en los siguientes
comportamientos:
AllowCombiningAuthorizeFilters
InputFormatterExceptionPolicy
[!code-csharpMain]
En el caso de las aplicaciones que encuentran cambios de comportamiento importantes, si se usan los
modificadores de compatibilidad adecuados:
Se podrá usar la versión más reciente y descartar cambios de comportamiento importantes específicos.
Se dispondrá de tiempo para actualizar la aplicación para que funcione con los cambios más recientes.
En los comentarios de código fuente de MvcOptions encontrará bien explicado qué ha cambiado y por qué
estos cambios son una mejora para la mayoría de los usuarios.
Próximamente habrá una versión ASP.NET Core 3.0. Los comportamientos anteriores admitidos por los
modificadores de compatibilidad se quitarán en esta versión 3.0. Estamos convencidos de que estos son
cambios positivos que beneficiarán a prácticamente todos los usuarios. Al presentarlos ahora, la mayoría de
las aplicaciones podrán empezar a sacar partido de ellos ya y los demás tendrán tiempo suficiente para
actualizar sus aplicaciones.

Servicios disponibles en Startup


El host de web proporciona algunos servicios que están disponibles para el constructor de clase Startup . La
aplicación agrega servicios adicionales a través de ConfigureServices . Los servicios de la aplicación y el host
están disponibles en Configure y en toda la aplicación.

El método Configure
El método Configure se utiliza para especificar la forma en que la aplicación responde a las solicitudes HTTP.
La canalización de solicitudes se configura mediante la adición de componentes de middleware a una
instancia de IApplicationBuilder. IApplicationBuilder está disponible para el método Configure , pero no
está registrado en el contenedor de servicios. Hospedaje crea un IApplicationBuilder y lo pasa
directamente a Configure (origen de referencia).
Las plantillas ASP.NET Core configuran la canalización con compatibilidad para una página de excepción
para desarrolladores, BrowserLink, páginas de error, archivos estáticos y ASP.NET MVC:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)


{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Error");
}

app.UseStaticFiles();

app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller}/{action=Index}/{id?}");
});
}

Cada método de extensión Use agrega un componente de middleware a la canalización de solicitudes. Por
ejemplo, el método de extensión UseMvc agrega el middleware de enrutamiento a la canalización de
solicitudes y establece MVC como controlador predeterminado.
Cada componente de middleware de la canalización de solicitudes es responsable de invocar al siguiente
componente de la canalización o de cortocircuitar la cadena en caso de ser necesario. Si el cortocircuito no
se produce a lo largo de la cadena de middleware, cada middleware tiene una segunda oportunidad de
procesar la solicitud antes de que se envíe al cliente.
Servicios adicionales, como IHostingEnvironment y ILoggerFactory , también se pueden especificar en la
firma del método. Cuando se especifican, esos servicios adicionales se insertan si están disponibles.
Para más información sobre cómo usar IApplicationBuilder y el orden de procesamiento de middleware,
vea Middleware.

Métodos de conveniencia
Los métodos de conveniencia ConfigureServices y Configure pueden usarse en lugar de especificar una
clase Startup . Varias llamadas a ConfigureServices se anexan entre sí. Varias llamadas a Configure usan la
última llamada al método.

public class Program


{
public static IHostingEnvironment HostingEnvironment { get; set; }
public static IConfiguration Configuration { get; set; }

public static void Main(string[] args)


{
BuildWebHost(args).Run();
}

public static IWebHost BuildWebHost(string[] args) =>


WebHost.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((hostingContext, config) =>
{
HostingEnvironment = hostingContext.HostingEnvironment;
Configuration = config.Build();
})
.ConfigureServices(services =>
{
services.AddMvc();
})
.Configure(app =>
{
if (HostingEnvironment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
}

// Configuration is available during startup. Examples:


// Configuration["key"]
// Configuration["subsection:suboption1"]

app.UseMvcWithDefaultRoute();
app.UseStaticFiles();
})
.Build();
}

Filtros de Startup
Use IStartupFilter para configurar el middleware al principio o al final de la canalización de middleware
Configure de una aplicación. IStartupFilter es útil para garantizar que un middleware se ejecuta antes o
después del middleware agregado por bibliotecas al principio o al final de la canalización de procesamiento
de solicitudes de la aplicación.
IStartupFilter implementa un método único, Configure, que recibe y devuelve
Action<IApplicationBuilder> . IApplicationBuilder define una clase para configurar la canalización de
solicitudes de una aplicación. Para más información, vea Creación de una canalización de middleware con
IApplicationBuilder.
Cada IStartupFilter implementa uno o más middleware en la canalización de solicitudes. Los filtros se
invocan en el orden en que se agregaron al contenedor de servicios. Los filtros pueden agregar middleware
antes o después de pasar el control al siguiente filtro, por lo que se anexan al principio o al final de la
canalización de la aplicación.
La aplicación de ejemplo (cómo descargar) muestra cómo se registra un middleware con IStartupFilter . La
aplicación de ejemplo incluye un middleware que establece un valor de opciones de un parámetro de cadena
de consulta:

public class RequestSetOptionsMiddleware


{
private readonly RequestDelegate _next;
private IOptions<AppOptions> _injectedOptions;

public RequestSetOptionsMiddleware(
RequestDelegate next, IOptions<AppOptions> injectedOptions)
{
_next = next;
_injectedOptions = injectedOptions;
}

public async Task Invoke(HttpContext httpContext)


{
Console.WriteLine("RequestSetOptionsMiddleware.Invoke");

var option = httpContext.Request.Query["option"];

if (!string.IsNullOrWhiteSpace(option))
{
_injectedOptions.Value.Option = WebUtility.HtmlEncode(option);
}

await _next(httpContext);
}
}

RequestSetOptionsMiddleware está configurado en las clase RequestSetOptionsStartupFilter :

public class RequestSetOptionsStartupFilter : IStartupFilter


{
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
{
return builder =>
{
builder.UseMiddleware<RequestSetOptionsMiddleware>();
next(builder);
};
}
}

IStartupFilter está registrado en el contenedor de servicios en ConfigureServices :

public void ConfigureServices(IServiceCollection services)


{
services.AddTransient<IStartupFilter, RequestSetOptionsStartupFilter>();
services.AddMvc();
}
Cuando se proporciona un parámetro de cadena de consulta para option , el middleware procesa el valor
asignado antes de que el middleware MVC represente la respuesta:

El orden de ejecución de middleware se establece según el orden de registros de IStartupFilter :


Varias implementaciones de IStartupFilter pueden interactuar con los mismos objetos. Si el orden es
importante, ordene los registros de servicio de IStartupFilter para que coincidan con el orden en que
se deben ejecutar los middleware.
Las bibliotecas pueden agregar middleware con una o varias implementaciones de IStartupFilter que
se ejecuten antes o después de otro middleware de aplicación registrado con IStartupFilter . Para
invocar un middleware IStartupFilter antes que un middleware agregado por el IStartupFilter de una
biblioteca, coloque el registro del servicio antes de que la biblioteca se agregue al contenedor de
servicios. Para invocarlo posteriormente, coloque el registro del servicio después de que se agregue la
biblioteca.

Agregar opciones de configuración en el inicio a partir de un


ensamblado externo
Una implementación de IHostingStartup permite agregar mejoras a una aplicación al iniciarla a partir de un
ensamblado externo fuera de la clase Startup de esta. Para obtener más información, consulte Mejora de
una aplicación a partir de un ensamblado externo.

Recursos adicionales
Hospedar aplicaciones de WPF
Uso de varios entornos
Middleware
Registro
Configuración
Clase StartupLoader: método FindStartupType (origen de referencia)
Inserción de dependencias en
ASP.NET Core
25/06/2018 • 34 minutes to read • Edit Online

Por Steve Smith y Scott Addie


ASP.NET Core está diseñado desde el principio para admitir y aprovechar la
inserción de dependencias. Las aplicaciones ASP.NET Core pueden aprovechar los
servicios del marco de trabajo integrado si los insertan en métodos de la clase
Startup, y los servicios de las aplicaciones también pueden configurarse para la
inserción. El contenedor de servicios predeterminados proporcionado por ASP.NET
Core incluye un conjunto de característica mínimas y no está diseñado para
reemplazar otros contenedores.
Vea o descargue el código de ejemplo (cómo descargarlo)

¿Qué es la inserción de dependencias?


La inserción de dependencias (DI) es una técnica para lograr un acoplamiento
flexible entre los objetos y sus colaboradores (o dependencias). En lugar de crear
directamente instancias de colaboradores o de usar referencias estáticas, los
objetos que una clase necesita para llevar a cabo sus acciones se proporcionan de
algún modo a dicha clase. A menudo, las clases declaran sus dependencias a través
de su constructor, lo que les permite seguir el principio de dependencias explícitas.
Este método se conoce como "inserción de constructor".
Cuando las clases se diseñan con DI en mente, se acoplan de manera más flexible
porque no tienen dependencias directas codificadas de forma rígida en sus
colaboradores. Esto sigue el principio de inversión de dependencias, que afirma
que "los módulos de nivel alto no deben depender de los módulos de nivel bajo;
ambos deben depender de abstracciones". En lugar de hacer referencia a
implementaciones específicas, las clases solicitan abstracciones (normalmente
interfaces ) que se les proporcionan cuando se construye la clase. La extracción de
dependencias en interfaces y el abastecimiento de implementaciones de estas
interfaces como parámetros son también ejemplos del modelo de diseño de
estrategias.
Cuando se diseña un sistema para el uso de DI, con muchas clases que solicitan sus
dependencias a través de su constructor (o propiedades), resulta útil tener una
clase dedicada a la creación de estas clases con sus dependencias asociadas. Estas
clases se conocen como contenedores o, más concretamente, contenedores de
inversión de control (IoC ) o contenedores de inserción de dependencias (DI). Un
contenedor es básicamente un generador que se encarga de proporcionar las
instancias de tipos que se le solicitan. Si un tipo determinado ha declarado que
tiene dependencias y el contenedor se ha configurado para proporcionar tipos de
dependencia, creará las dependencias como parte del proceso de creación de la
instancia solicitada. De esta manera, se pueden proporcionar gráficos de
dependencias complejos a las clases sin necesidad de construir objetos codificados
de forma rígida. Además de crear objetos con sus dependencias, los contenedores
suelen administrar la duración de los objetos dentro de la aplicación.
ASP.NET Core incluye un simple contenedor integrado (representado por la
interfaz IServiceProvider ) que admite la inserción de constructor de forma
predeterminada, y ASP.NET hace que determinados servicios estén disponibles a
través de DI. El contenedor de ASP.NET hace referencia a los tipos que administra
como servicios. De ahora en adelante en este artículo, el término servicios hará
referencia a los tipos administrados por el contenedor de IoC de ASP.NET Core.
Los servicios del contenedor integrado se configuran en el método
ConfigureServices de la clase Startup de la aplicación.

NOTE
Martin Fowler ha escrito un amplio artículo sobre los contenedores de inversión de
control y el patrón de inserción de dependencias. En los Modelos y prácticas de Microsoft
también encontrará una descripción excelente de la inserción de dependencias.

NOTE
Este artículo trata sobre la inserción de dependencias tal como se aplica a todas las
aplicaciones ASP.NET. La inserción de dependencias dentro de controladores de MVC se
trata en Inserción de dependencias y controladores.

Comportamiento de inserción de constructor


La inserción de constructor requiere que el constructor en cuestión sea público. En
caso contrario, la aplicación producirá una InvalidOperationException :

A suitable constructor for type 'YourType' couldn't be located. Ensure the type is
concrete and services are registered for all parameters of a public constructor.
(No se pudo encontrar un constructor adecuado para el tipo "SuTipo".
Asegúrese de que el tipo sea concreto y de que los servicios estén registrados
para todos los parámetros de un constructor público.)

La inserción de constructor requiere que solo exista un constructor aplicable. Se


admiten las sobrecargas de constructor, pero solo puede existir una sobrecarga
cuyos argumentos pueda cumplir la inserción de dependencias. Si existe más de
una, la aplicación producirá una InvalidOperationException :

Multiple constructors accepting all given argument types have been found in
type 'YourType'. Multiple constructors accepting all given argument types have
been found in type 'YourType'. There should only be one applicable constructor.
(Se han encontrado en el tipo "SuTipo" varios constructores que aceptan todos
los tipos de argumento especificados. Solo debe haber un constructor
aplicable.)

Los constructores pueden aceptar argumentos que no se proporcionan mediante la


inserción de dependencias, pero deben admitir valores predeterminados. Por
ejemplo:
// throws InvalidOperationException: Unable to resolve service for type
'System.String'...
public CharactersController(ICharacterRepository characterRepository, string
title)
{
_characterRepository = characterRepository;
_title = title;
}

// runs without error


public CharactersController(ICharacterRepository characterRepository, string
title = "Characters")
{
_characterRepository = characterRepository;
_title = title;
}

Uso de servicios proporcionados por el marco de


trabajo
El método ConfigureServices de la clase Startup se encarga de definir los
servicios que usará la aplicación, incluidas las características de plataforma como
Entity Framework Core y ASP.NET Core MVC. Inicialmente, el valor
IServiceCollection proporcionado a ConfigureServices tiene los siguientes
servicios definidos (en función de cómo se configurara el host):

TIPO DE SERVICIO PERÍODO DE DURACIÓN

Microsoft.AspNetCore.Hosting.IHostingEnvir Singleton
onment

Microsoft.Extensions.Logging.ILoggerFactory Singleton

Microsoft.Extensions.Logging.ILogger<T> Singleton

Microsoft.AspNetCore.Hosting.Builder.IApplic Transitorio
ationBuilderFactory

Microsoft.AspNetCore.Http.IHttpContextFact Transitorio
ory

Microsoft.Extensions.Options.IOptions<T> Singleton

System.Diagnostics.DiagnosticSource Singleton

System.Diagnostics.DiagnosticListener Singleton

Microsoft.AspNetCore.Hosting.IStartupFilter Transitorio

Microsoft.Extensions.ObjectPool.ObjectPoolP Singleton
rovider

Microsoft.Extensions.Options.IConfigureOpti Transitorio
ons<T>
TIPO DE SERVICIO PERÍODO DE DURACIÓN

Microsoft.AspNetCore.Hosting.Server.IServer Singleton

Microsoft.AspNetCore.Hosting.IStartup Singleton

Microsoft.AspNetCore.Hosting.IApplicationLi Singleton
fetime

A continuación se muestra un ejemplo de cómo agregar servicios adicionales al


contenedor mediante una serie de métodos de extensión como AddDbContext ,
AddIdentity y AddMvc .

// This method gets called by the runtime. Use this method to add services to
the container.
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddDbContext<ApplicationDbContext>(options =>

options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();

services.AddMvc();

// Add application services.


services.AddTransient<IEmailSender, AuthMessageSender>();
services.AddTransient<ISmsSender, AuthMessageSender>();
}

Las características y el software intermedio proporcionados por ASP.NET, como


MVC, siguen la convención de usar un solo método de extensión
AddNombreDelServicio para registrar todos los servicios requeridos por esa
característica.

TIP
Puede solicitar determinados servicios proporcionados por el marco de trabajo dentro de
métodos Startup mediante sus listas de parámetros. Vea Inicio de la aplicación para
obtener más detalles.

Registrar servicios
Puede registrar sus propios servicios de aplicación de la manera siguiente. El
primer tipo genérico representa el tipo (normalmente una interfaz) que se solicitará
desde el contenedor. El segundo tipo genérico representa el tipo concreto del cual
creará una instancia el contenedor y que se usará para responder a dichas
solicitudes.

services.AddTransient<IEmailSender, AuthMessageSender>();
services.AddTransient<ISmsSender, AuthMessageSender>();
NOTE
Cada método de extensión services.Add<ServiceName> agrega servicios (y
potencialmente los configura). Por ejemplo, services.AddMvc() agrega los servicios que
requiere MVC. Se recomienda que siga esta convención y coloque los métodos de
extensión en el espacio de nombres Microsoft.Extensions.DependencyInjection para
encapsular grupos de registros del servicio.

El método AddTransient se usa para asignar tipos abstractos a servicios concretos


de los que se crean instancias por separado para cada objeto que lo requiera. Esto
se conoce como la duración del servicio. A continuación se describen opciones de
duración adicionales. Es importante elegir una duración adecuada para cada uno
de los servicios que se registren. ¿Debe proporcionarse una nueva instancia del
servicio a cada clase que lo solicite? ¿Debe usarse una instancia en una solicitud
web determinada? ¿O debe usarse una sola instancia para la duración de la
aplicación?
En el ejemplo incluido en este artículo, hay un controlador simple que muestra los
nombres de caracteres, denominado CharactersController . Su método Index
muestra la lista actual de caracteres que se han almacenado en la aplicación e
inicializa la colección con unos cuantos caracteres, si no existe ninguno. Tenga en
cuenta que, aunque esta aplicación usa Entity Framework Core y la clase
ApplicationDbContext para su persistencia, ninguno es evidente en el controlador.
En su lugar, el mecanismo de acceso a datos específico se ha abstraído detrás de
una interfaz, ICharacterRepository , que sigue el modelo de repositorio. Se solicita
una instancia de ICharacterRepository mediante el constructor y se asigna a un
campo privado, que después se usa para tener acceso a caracteres, según sea
necesario.
public class CharactersController : Controller
{
private readonly ICharacterRepository _characterRepository;

public CharactersController(ICharacterRepository characterRepository)


{
_characterRepository = characterRepository;
}

// GET: /characters/
public IActionResult Index()
{
PopulateCharactersIfNoneExist();
var characters = _characterRepository.ListAll();

return View(characters);
}

private void PopulateCharactersIfNoneExist()


{
if (!_characterRepository.ListAll().Any())
{
_characterRepository.Add(new Character("Darth Maul"));
_characterRepository.Add(new Character("Darth Vader"));
_characterRepository.Add(new Character("Yoda"));
_characterRepository.Add(new Character("Mace Windu"));
}
}
}

ICharacterRepository define los dos métodos que el controlador necesita para


funcionar con instancias de Character .

using System.Collections.Generic;
using DependencyInjectionSample.Models;

namespace DependencyInjectionSample.Interfaces
{
public interface ICharacterRepository
{
IEnumerable<Character> ListAll();
void Add(Character character);
}
}

Esta interfaz la implementa a su vez un tipo concreto, CharacterRepository , que se


usa en tiempo de ejecución.

NOTE
La manera en que se usa la inserción de dependencias con la clase
CharacterRepository es un modelo general que puede seguir para todos los servicios
de aplicación, no solo en "repositorios" o clases de acceso a datos.
using System.Collections.Generic;
using System.Linq;
using DependencyInjectionSample.Interfaces;

namespace DependencyInjectionSample.Models
{
public class CharacterRepository : ICharacterRepository
{
private readonly ApplicationDbContext _dbContext;

public CharacterRepository(ApplicationDbContext dbContext)


{
_dbContext = dbContext;
}

public IEnumerable<Character> ListAll()


{
return _dbContext.Characters.AsEnumerable();
}

public void Add(Character character)


{
_dbContext.Characters.Add(character);
_dbContext.SaveChanges();
}
}
}

Tenga en cuenta que CharacterRepository solicita ApplicationDbContext en su


constructor. No es raro que la inserción de dependencias se use encadenada de
este modo, donde cada dependencia solicitada solicita a su vez sus propias
dependencias. El contenedor se encarga de resolver todas las dependencias del
gráfico y devolver el servicio totalmente resuelto.

NOTE
El proceso de creación del objeto solicitado, de todos los objetos que necesita y de todos
los objetos que estos necesitan suele denominarse gráfico de objetos. Del mismo modo,
el conjunto colectivo de dependencias que deben resolverse suele denominarse árbol de
dependencias o gráfico de dependencias.

En este caso, es necesario registrar ICharacterRepository y ApplicationDbContext


con el contenedor de servicios de ConfigureServices en Startup .
ApplicationDbContext se configura con la llamada al método de extensión
AddDbContext<T> . En el código siguiente se muestra el registro del tipo
CharacterRepository .
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseInMemoryDatabase()
);

// Add framework services.


services.AddMvc();

// Register application services.


services.AddScoped<ICharacterRepository, CharacterRepository>();
services.AddTransient<IOperationTransient, Operation>();
services.AddScoped<IOperationScoped, Operation>();
services.AddSingleton<IOperationSingleton, Operation>();
services.AddSingleton<IOperationSingletonInstance>(new
Operation(Guid.Empty));
services.AddTransient<OperationService, OperationService>();
}

Es necesario agregar contextos de Entity Framework al contenedor de servicios


mediante la duración Scoped . Esto se lleva a cabo automáticamente si usa los
métodos auxiliares, tal como se ha indicado anteriormente. Los repositorios que
vayan a usar Entity Framework deben usar la misma duración.

WARNING
Debe ser especialmente cauteloso al resolver un servicio Scoped desde un singleton. Es
probable que en este caso el servicio tenga un estado incorrecto al procesar las
solicitudes posteriores.

Los servicios que tengan dependencias deben registrarlas en el contenedor. Si el


constructor de un servicio requiere un primitivo, como string , se puede insertar
mediante la configuración y el patrón de opciones.

Duración de los servicios y opciones de registro


Los servicios de ASP.NET pueden configurarse con las duraciones siguientes:
Transitoria
Los servicios de duración transitoria se crean cada vez que solicitan. Esta duración
funciona mejor para servicios sin estado ligeros.
Con ámbito
Los servicios de duración con ámbito se crean una vez por solicitud.

WARNING
Si usa un servicio con ámbito en un middleware, inserte el servicio en el método Invoke
o InvokeAsync . No lo inserte a través de la inserción de constructores, porque ello hace
que el servicio se comporte como un singleton.

Singleton
Los servicios de duración de singleton se crean la primera vez que se solicitan (o
cuando se ejecuta ConfigureServices si se especifica ahí una instancia) y todas las
solicitudes posteriores usan la misma instancia. Si la aplicación requiere un
comportamiento de singleton, se recomienda permitir que el contenedor de
servicios administre la duración del servicio, en lugar de implementar el patrón de
diseño de singleton y administrar por sí mismo la duración del objeto en la clase.
Los servicios se pueden registrar con el contenedor de varias maneras. Ya hemos
visto cómo registrar una implementación de servicio con un tipo determinado
mediante la especificación del tipo concreto que se va a usar. Además, se puede
especificar un generador, que se usará para crear la instancia a petición. El tercer
método consiste en especificar directamente la instancia del tipo que se va a usar,
en cuyo caso el contenedor nunca intentará crear una instancia (ni la eliminará).
Para mostrar la diferencia entre estas duraciones y opciones de registro, imagine
una interfaz simple que representa una o varias tareas como una operación con un
identificador único, OperationId . Según cómo se configure la duración de este
servicio, el contenedor proporcionará las mismas instancias o instancias diferentes
del servicio a la clase que lo solicita. Para dejar claro qué duración se solicita,
crearemos un tipo por opción de duración:

using System;

namespace DependencyInjectionSample.Interfaces
{
public interface IOperation
{
Guid OperationId { get; }
}

public interface IOperationTransient : IOperation


{
}
public interface IOperationScoped : IOperation
{
}
public interface IOperationSingleton : IOperation
{
}
public interface IOperationSingletonInstance : IOperation
{
}
}

Estas interfaces se implementan mediante una sola clase, Operation , que acepta un
Guid en su constructor, o usa un Guid nuevo si no se proporciona ninguno.

Después, en ConfigureServices , se agrega cada tipo al contenedor según su


duración con nombre:

services.AddScoped<ICharacterRepository, CharacterRepository>();
services.AddTransient<IOperationTransient, Operation>();
services.AddScoped<IOperationScoped, Operation>();
services.AddSingleton<IOperationSingleton, Operation>();
services.AddSingleton<IOperationSingletonInstance>(new
Operation(Guid.Empty));
services.AddTransient<OperationService, OperationService>();
}

Tenga en cuenta que el servicio IOperationSingletonInstance usa una instancia


específica con un identificador conocido de Guid.Empty para que quede claro
cuándo se usa este tipo (su GUID contendrá únicamente ceros). También hemos
registrado un OperationService que depende de cada uno de los otros tipos
Operation , para que quede claro dentro de una solicitud si este servicio obtiene la
misma instancia que el controlador o una nueva para cada tipo de operación. Lo
único que hace este servicio es exponer sus dependencias como propiedades, para
que se puedan mostrar en la vista.

using DependencyInjectionSample.Interfaces;

namespace DependencyInjectionSample.Services
{
public class OperationService
{
public IOperationTransient TransientOperation { get; }
public IOperationScoped ScopedOperation { get; }
public IOperationSingleton SingletonOperation { get; }
public IOperationSingletonInstance SingletonInstanceOperation { get; }

public OperationService(IOperationTransient transientOperation,


IOperationScoped scopedOperation,
IOperationSingleton singletonOperation,
IOperationSingletonInstance instanceOperation)
{
TransientOperation = transientOperation;
ScopedOperation = scopedOperation;
SingletonOperation = singletonOperation;
SingletonInstanceOperation = instanceOperation;
}
}
}

Para mostrar a la aplicación las duraciones de los objetos dentro de las solicitudes
individuales independientes y entre estas, el ejemplo incluye un
OperationsController que solicita cada tipo de IOperation , así como un
OperationService . Después, la acción Index muestra todos los valores del
controlador y del servicio OperationId .
using DependencyInjectionSample.Interfaces;
using DependencyInjectionSample.Services;
using Microsoft.AspNetCore.Mvc;

namespace DependencyInjectionSample.Controllers
{
public class OperationsController : Controller
{
private readonly OperationService _operationService;
private readonly IOperationTransient _transientOperation;
private readonly IOperationScoped _scopedOperation;
private readonly IOperationSingleton _singletonOperation;
private readonly IOperationSingletonInstance
_singletonInstanceOperation;

public OperationsController(OperationService operationService,


IOperationTransient transientOperation,
IOperationScoped scopedOperation,
IOperationSingleton singletonOperation,
IOperationSingletonInstance singletonInstanceOperation)
{
_operationService = operationService;
_transientOperation = transientOperation;
_scopedOperation = scopedOperation;
_singletonOperation = singletonOperation;
_singletonInstanceOperation = singletonInstanceOperation;
}

public IActionResult Index()


{
// viewbag contains controller-requested services
ViewBag.Transient = _transientOperation;
ViewBag.Scoped = _scopedOperation;
ViewBag.Singleton = _singletonOperation;
ViewBag.SingletonInstance = _singletonInstanceOperation;

// operation service has its own requested services


ViewBag.Service = _operationService;
return View();
}
}
}

Ahora se realizan dos solicitudes diferentes a esta acción del controlador:


Observe cuál de los valores OperationId varía dentro de una solicitud y entre
solicitudes.
Los objetos transitorios son siempre diferentes, ya que se proporciona una
nueva instancia a cada controlador y servicio.
Los objetos con ámbito son iguales dentro de una solicitud, pero varían
entre solicitudes.
Los objetos singleton son iguales para todos los objetos y solicitudes
(independientemente de si se proporciona una instancia en
ConfigureServices )

Resolver un servicio con ámbito dentro del ámbito


de la aplicación
Cree un IServiceScope con IServiceScopeFactory.CreateScope para resolver un
servicio con ámbito dentro del ámbito de la aplicación. Este método resulta útil
para tener acceso a un servicio con ámbito durante el inicio para realizar tareas de
inicialización. En el siguiente ejemplo se indica cómo obtener un contexto para
MyScopedService en Program.Main :
public static void Main(string[] args)
{
var host = BuildWebHost(args);

using (var serviceScope = host.Services.CreateScope())


{
var services = serviceScope.ServiceProvider;

try
{
var serviceContext = services.GetRequiredService<MyScopedService>
();
// Use the context here
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred.");
}
}

host.Run();
}

Validación del ámbito


Cuando la aplicación se ejecuta en el entorno de desarrollo de ASP.NET Core 2.0 o
posterior, el proveedor de servicios predeterminado realiza comprobaciones para
confirmar lo siguiente:
Los servicios con ámbito no se resuelven directa o indirectamente desde el
proveedor de servicios raíz.
Los servicios con ámbito no se insertan directa o indirectamente en singletons.
El proveedor de servicios raíz se crea cuando se llama a BuildServiceProvider. La
vigencia del proveedor de servicios raíz es la misma que la de la aplicación o el
servidor cuando el proveedor se inicia con la aplicación, y se elimina cuando la
aplicación se cierra.
De la eliminación de los servicios con ámbito se encarga el contenedor que los
creó. Si un servicio con ámbito se crea en el contenedor raíz, su vigencia sube a la
del singleton, ya que solo lo puede eliminar el contenedor raíz cuando la aplicación
o el servidor se cierran. Al validar los ámbitos de servicio, este tipo de situaciones
se detectan cuando se llama a BuildServiceProvider .
Para obtener más información, vea Validación del ámbito en el tema de host web.

Servicios de solicitud
Los servicios disponibles en una solicitud de ASP.NET desde HttpContext se
exponen mediante la colección RequestServices .
Los servicios de solicitud representan los servicios que se configuran y se solicitan
como parte de la aplicación. Cuando los objetos especifican dependencias, estas se
cumplen mediante los tipos que se encuentran en RequestServices , no en
ApplicationServices .

Por lo general, no debe usar estas propiedades directamente. Se recomienda que


solicite los tipos que las clases necesitan mediante el constructor de la clase y que
deje que el marco de trabajo inserte estas dependencias. Esto da como resultado
clases más fáciles de probar (vea Pruebas y depuración) y acopladas de manera
más flexible.

NOTE
Se recomienda que solicite las dependencias como parámetros del constructor para
obtener acceso a la colección RequestServices .

Diseño de servicios para la inserción de


dependencias
Debe diseñar los servicios de modo que usen la inserción de dependencias para
obtener sus colaboradores. Esto significa que debe evitar el uso de llamadas a
métodos estáticos con estado (lo que ocasiona un problema en el código
denominado adhesión estática) y la creación de instancias directa de clases
dependientes dentro de los servicios. Puede resultarle útil recordar la frase "lo
nuevo se pega" al decidir si va a crear una instancia de un tipo o solicitarlo
mediante la inserción de dependencias. Si sigue los principios SOLID del diseño
orientado a objetos, las clases tenderán de forma natural a ser pequeñas, estarán
correctamente factorizadas y podrán probarse fácilmente.
¿Qué ocurre si descubre que en sus clases suelen insertarse demasiadas
dependencias? Esto suele indicar que la clase intenta realizar demasiadas acciones
y que probablemente infringe el principio de responsabilidad única (SRP ). Mueva
algunas de las responsabilidades de la clase a una nueva para intentar
refactorizarla. Tenga en cuenta que las clases Controller deben centrarse en
aspectos de la interfaz de usuario, por lo que los detalles de implementación de las
reglas de negocio y del acceso a datos se deben mantener en las clases pertinentes
para cada uno de estos aspectos.
En lo que respecta al acceso a datos en concreto, puede insertar DbContext en los
controladores (siempre y cuando haya agregado EF al contenedor de servicios en
ConfigureServices ). Algunos desarrolladores prefieren usar una interfaz de
repositorio para la base de datos en lugar de insertar DbContext directamente. Si
usa una interfaz para encapsular la lógica de acceso a datos en un solo lugar, puede
minimizar el número de lugares que tendrá que cambiar cuando cambie la base de
datos.
Eliminación de servicios
El contenedor llamará a Dispose para los tipos IDisposable que cree. A pesar de
todo, si agrega usted mismo una instancia al contenedor, no se eliminará.
Ejemplo:
// Services implement IDisposable:
public class Service1 : IDisposable {}
public class Service2 : IDisposable {}
public class Service3 : IDisposable {}

public interface ISomeService {}


public class SomeServiceImplementation : ISomeService, IDisposable {}

public void ConfigureServices(IServiceCollection services)


{
// container will create the instance(s) of these types and will dispose
them
services.AddScoped<Service1>();
services.AddSingleton<Service2>();
services.AddSingleton<ISomeService>(sp => new SomeServiceImplementation());

// container didn't create instance so it will NOT dispose it


services.AddSingleton<Service3>(new Service3());
services.AddSingleton(new Service3());
}

NOTE
En la versión 1.0, el contenedor llamaba a Dispose en todos los objetos IDisposable ,
incluidos aquellos que no había creado.

Reemplazo del contenedor de servicios


predeterminado
El contenedor de servicios integrado está pensado para atender las necesidades
básicas del marco de trabajo y de la mayoría de las aplicaciones de consumidor
compiladas en él. Aun así, los desarrolladores pueden reemplazar el contenedor
integrado por su contenedor preferido. El método ConfigureServices suele
devolver void , pero si se cambia su firma para que devuelva IServiceProvider , se
puede configurar y devolver un contenedor diferente. Hay muchos contenedores
de IoC disponibles para .NET. En este ejemplo, se usa el paquete Autofac.
En primer lugar, instale los paquetes de contenedor adecuados:
Autofac
Autofac.Extensions.DependencyInjection

Después, configure el contenedor en ConfigureServices y devuelva un


IServiceProvider :
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddMvc();
// Add other framework services

// Add Autofac
var containerBuilder = new ContainerBuilder();
containerBuilder.RegisterModule<DefaultModule>();
containerBuilder.Populate(services);
var container = containerBuilder.Build();
return new AutofacServiceProvider(container);
}

NOTE
Cuando se usa un contenedor de inserción de dependencias de terceros, debe cambiar
ConfigureServices para que devuelva IServiceProvider en lugar de void .

Por último, configure Autofac de la forma habitual en DefaultModule :

public class DefaultModule : Module


{
protected override void Load(ContainerBuilder builder)
{
builder.RegisterType<CharacterRepository>().As<ICharacterRepository>();
}
}

En tiempo de ejecución, se usará Autofac para resolver tipos e insertar


dependencias. Más información sobre el uso de Autofac y ASP.NET Core.
Seguridad para subprocesos
Los servicios de singleton deben ser seguros para subprocesos. Si un servicio de
singleton tiene una dependencia en un servicio transitorio, es posible que este
también deba ser seguro para subprocesos, según cómo lo use el singleton.

Recomendaciones
Cuando trabaje con la inserción de dependencias, tenga en cuenta las
recomendaciones siguientes:
La inserción de dependencias está destinada a objetos que tienen
dependencias complejas. Los controladores, los servicios, los adaptadores y
los repositorios son ejemplos de objetos que podrían agregarse a la
inserción de dependencias.
Evite almacenar datos y configuraciones directamente en la inserción de
dependencias. Por ejemplo, el carro de la compra de un usuario no debería
agregarse al contenedor de servicios. La configuración debe usar el patrón
de opciones. Del mismo modo, evite los objetos de tipo "contenedor de
datos" que solo existen para permitir el acceso a otro objeto. Es mejor
solicitar el elemento real que se necesita mediante la inserción de
dependencias, si es posible.
Evite el acceso estático a los servicios.
Evite la ubicación de servicios en el código de la aplicación.
Evite el acceso estático a HttpContext .
Al igual que sucede con todas las recomendaciones, podría verse en una situación
que le obligue a ignorar alguna de ellas. Por lo que sabemos las excepciones son
muy poco frecuentes, ya que suelen ser casos muy especiales del propio marco de
trabajo.
La inserción de dependencias es una alternativa al uso de patrones de acceso a
objetos estáticos o globales. No podrá aprovechar las ventajas de la inserción de
dependencias si la combina con el acceso a objetos estáticos.

Recursos adicionales
Inserción de dependencias en vistas
Inserción de dependencias en controladores
Inserción de dependencias en controladores de requisitos
Inicio de aplicaciones
Prueba y depuración
Factory-based middleware activation (Activación de middleware basada en
Factory)
Escritura de código limpio en ASP.NET Core con inserción de dependencias
(MSDN )
Preludio del diseño de aplicaciones administradas por contenedor: ¿cuál es el
lugar del contenedor?
Principio de dependencias explícitas
Los contenedores de inversión de control y el patrón de inserción de
dependencias (Fowler)
Middleware de ASP.NET Core
21/06/2018 • 19 minutes to read • Edit Online

Por Rick Anderson y Steve Smith


Vea o descargue el código de ejemplo (cómo descargarlo)

¿Qué es el middleware?
El middleware es un software que se ensambla a una canalización de una aplicación para controlar las solicitudes y
las respuestas. Cada componente puede hacer lo siguiente:
Elegir si se pasa la solicitud al siguiente componente de la canalización.
Realizar trabajos antes y después de invocar al siguiente componente de la canalización.
Los delegados de solicitudes se usan para crear la canalización de solicitudes. Estos también controlan las
solicitudes HTTP.
Los delegados de solicitudes se configuran con los métodos de extensión Run, Map y Use. Un delegado de
solicitudes se puede especificar en línea como un método anónimo (denominado middleware en línea) o se puede
definir en una clase reutilizable. Estas clases reutilizables y métodos anónimos en línea son middleware o
componentes de middleware. Cada componente de middleware de la canalización de solicitudes es responsable de
invocar al siguiente componente de la canalización o de cortocircuitar la cadena en caso de ser necesario.
En Migración de módulos HTTP a middleware se explica la diferencia entre las canalizaciones de solicitudes en
ASP.NET Core y ASP.NET 4.x y se proporcionan más ejemplos de middleware.

Creación de una canalización de middleware con IApplicationBuilder


La canalización de solicitudes de ASP.NET Core está compuesta por una secuencia de delegados de solicitudes, a
los que se llama uno después de otro, tal y como se muestra en este diagrama (el hilo de ejecución sigue las flechas
negras):

Cada delegado puede realizar operaciones antes y después del siguiente. También puede decidir no pasar una
solicitud al siguiente delegado, lo que se denomina "cortocircuitar" la canalización de solicitudes. Este proceso es
necesario muchas veces, ya que previene la realización de trabajo innecesario. Por ejemplo, el middleware de
archivos estáticos puede devolver una solicitud para un archivo estático y cortocircuitar el resto de la canalización.
Los delegados que controlan excepciones deben llamarse al principio de la canalización para que puedan capturar
las excepciones que se producen en las fases siguientes de la canalización.
La aplicación ASP.NET Core más sencilla posible configura un solo delegado de solicitudes que controla todas las
solicitudes. En este caso no se incluye una canalización de solicitudes real. En su lugar, solo se llama a una única
función anónima en respuesta a todas las solicitudes HTTP.

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;

public class Startup


{
public void Configure(IApplicationBuilder app)
{
app.Run(async context =>
{
await context.Response.WriteAsync("Hello, World!");
});
}
}

El primer delegado app.Run finaliza la canalización.


Puede encadenar varios delegados de solicitudes con app.Use. El parámetro next representa el siguiente delegado
de la canalización. Recuerde que puede cortocircuitar la canalización si no llama al siguiente parámetro.
Normalmente puede realizar acciones antes y después del siguiente delegado, tal y como se muestra en este
ejemplo:

public class Startup


{
public void Configure(IApplicationBuilder app)
{
app.Use(async (context, next) =>
{
// Do work that doesn't write to the Response.
await next.Invoke();
// Do logging or other work that doesn't write to the Response.
});

app.Run(async context =>


{
await context.Response.WriteAsync("Hello from 2nd delegate.");
});
}
}
WARNING
No llame a next.Invoke después de haber enviado la respuesta al cliente. Se producirá una excepción si se modifica
HttpResponse después de haber iniciado la respuesta. Por ejemplo, se producirá una excepción al realizar cambios como el
establecimiento de encabezados, el código de estado, etc. Si escribe en el cuerpo de la respuesta después de llamar a next :
Puede provocar una infracción del protocolo. Por ejemplo, si escribe más de la longitud content-length establecida.
Puede dañar el formato del cuerpo. Por ejemplo, si escribe un pie de página en HTML en un archivo CSS.
HttpResponse.HasStarted es una sugerencia útil para indicar si se han enviado los encabezados o si se han realizado escrituras
en el cuerpo.

Ordenación
El orden en el que se agregan los componentes de middleware en el método Configure define el orden en el que
se invocarán en las solicitudes y el orden inverso de la respuesta. Este orden es esencial por motivos de seguridad,
rendimiento y funcionalidad.
El método Configure (que se muestra aquí) agrega los siguientes componentes de middleware:
1. Control de errores y excepciones
2. Servidor de archivos estáticos
3. Autenticación
4. MVC
ASP.NET Core 2.x
ASP.NET Core 1.x

public void Configure(IApplicationBuilder app)


{
app.UseExceptionHandler("/Home/Error"); // Call first to catch exceptions
// thrown in the following middleware.

app.UseStaticFiles(); // Return static files and end pipeline.

app.UseAuthentication(); // Authenticate before you access


// secure resources.

app.UseMvcWithDefaultRoute(); // Add MVC to the request pipeline.


}

En el código anterior, UseExceptionHandler es el primer componente de middleware que se agrega a la canalización.


Por tanto, captura todas las excepciones que se puedan producir en las llamadas posteriores.
El middleware de archivos estáticos se llama al principio de la canalización para que pueda controlar solicitudes y
realizar cortocircuitos sin pasar por los componentes restantes. Este middleware no proporciona comprobaciones
de autorización. Los archivos que proporciona, incluidos los de wwwroot, están disponibles de forma pública.
Consulte Archivos estáticos para obtener más información sobre cómo proteger este tipo de archivos.
ASP.NET Core 2.x
ASP.NET Core 1.x
Si el middleware de archivos estáticos no controla la solicitud, se pasa al middleware de identidad (
app.UseAuthentication ), que realiza la autenticación. Este middleware no cortocircuita las solicitudes sin
autenticación. Aunque autentique solicitudes, la autorización (y el rechazo) se producen después de que MVC
seleccione una página de Razor o un control y una acción concretos.
En el ejemplo siguiente se muestra un orden de middleware en el que el middleware de archivos estáticos controla
las solicitudes de archivos estáticos antes del middleware de compresión de respuestas. Los archivos estáticos no
se comprimen con este orden de middleware. Las respuestas MVC de UseMvcWithDefaultRoute se pueden
comprimir.

public void Configure(IApplicationBuilder app)


{
app.UseStaticFiles(); // Static files not compressed
// by middleware.
app.UseResponseCompression();
app.UseMvcWithDefaultRoute();
}

Use, Run y Map


Puede configurar la canalización HTTP con Use , Run y Map . El método Use puede cortocircuitar la canalización
(solo si no llama a un delegado de solicitudes next ). Run es una convención y es posible que algunos
componentes de middleware expongan métodos Run[Middleware] que se ejecutan al final de la canalización.
Las extensiones Map* se usan como convenciones para la creación de ramas en la canalización. Map crea una rama
de la canalización de solicitudes según las coincidencias de la ruta de solicitud proporcionada. Si la ruta de solicitud
comienza con la ruta proporcionada, se ejecuta la creación de la rama.

public class Startup


{
private static void HandleMapTest1(IApplicationBuilder app)
{
app.Run(async context =>
{
await context.Response.WriteAsync("Map Test 1");
});
}

private static void HandleMapTest2(IApplicationBuilder app)


{
app.Run(async context =>
{
await context.Response.WriteAsync("Map Test 2");
});
}

public void Configure(IApplicationBuilder app)


{
app.Map("/map1", HandleMapTest1);

app.Map("/map2", HandleMapTest2);

app.Run(async context =>


{
await context.Response.WriteAsync("Hello from non-Map delegate. <p>");
});
}
}

En la siguiente tabla se muestran las solicitudes y las respuestas de http://localhost:1234 con el código anterior:

SOLICITUD RESPUESTA

localhost:1234 Saludos del delegado sin Map.


SOLICITUD RESPUESTA

localhost:1234/map1 Prueba 1 de Map

localhost:1234/map2 Prueba 2 de Map

localhost:1234/map3 Saludos del delegado sin Map.

Cuando se usa , los segmentos de ruta que coincidan se eliminan de


Map HttpRequest.Path y se anexan a
HttpRequest.PathBase por cada solicitud.

MapWhen crea una rama de la canalización de solicitudes según el resultado del predicado proporcionado. Se
puede usar cualquier predicado de tipo Func<HttpContext, bool> para asignar solicitudes a nuevas ramas de la
canalización. En el ejemplo siguiente se usa un predicado para detectar la presencia de una branch variable de
cadena de consulta:

public class Startup


{
private static void HandleBranch(IApplicationBuilder app)
{
app.Run(async context =>
{
var branchVer = context.Request.Query["branch"];
await context.Response.WriteAsync($"Branch used = {branchVer}");
});
}

public void Configure(IApplicationBuilder app)


{
app.MapWhen(context => context.Request.Query.ContainsKey("branch"),
HandleBranch);

app.Run(async context =>


{
await context.Response.WriteAsync("Hello from non-Map delegate. <p>");
});
}
}

En la siguiente tabla se muestran las solicitudes y las respuestas de http://localhost:1234 con el código anterior:

SOLICITUD RESPUESTA

localhost:1234 Saludos del delegado sin Map.

localhost:1234/?branch=master Rama usada = master

Map admite la anidación, por ejemplo:


app.Map("/level1", level1App => {
level1App.Map("/level2a", level2AApp => {
// "/level1/level2a"
//...
});
level1App.Map("/level2b", level2BApp => {
// "/level1/level2b"
//...
});
});

Map puede hacer coincidir varios segmentos a la vez, por ejemplo:

app.Map("/level1/level2", HandleMultiSeg);

Middleware integrado
ASP.NET Core incluye los siguientes componentes de middleware, así como una descripción del orden en el que se
deberían agregar:

SOFTWARE INTERMEDIO DESCRIPTION ORDEN

Autenticación Proporciona compatibilidad con Antes de que se necesite


autenticación. HttpContext.User . Terminal para
devoluciones de llamadas OAuth.

CORS Configura el uso compartido de Antes de los componentes que usan


recursos entre orígenes. CORS.

Diagnóstico Configura el diagnóstico. Antes de los componentes que generan


errores.

Encabezados reenviados Reenvía encabezados con proxy a la Antes de los componentes que
solicitud actual. consumen los campos actualizados
(ejemplos: esquema, host, cliente, IP y
método).

Invalidación del método HTTP Permite que una solicitud POST entrante Antes de los componentes que
invalide el método. consumen el método actualizado.

Redireccionamiento de HTTPS Redireccione todas las solicitudes HTTP a Antes de los componentes que
HTTPS (ASP.NET Core 2.1 o posterior). consumen la dirección URL.

Seguridad de transporte estricta de Middleware de mejora de seguridad que Antes de que se envíen las respuestas y
HTTP (HSTS) agrega un encabezado de respuesta después de los componentes que
especial (ASP.NET Core 2.1 o posterior). modifican las solicitudes (por ejemplo,
encabezados reenviados, reescritura de
URL).

Almacenamiento en caché de Proporciona compatibilidad con la Antes de los componentes que


respuestas captura de respuestas. requieren el almacenamiento en caché.

Compresión de respuesta Proporciona compatibilidad con la Antes de los componentes que


compresión de respuestas. requieren compresión.
SOFTWARE INTERMEDIO DESCRIPTION ORDEN

Localización de solicitudes Proporciona compatibilidad con Antes de los componentes que


ubicación. dependen de la ubicación.

Enrutamiento Define y restringe las rutas de la Terminal para rutas que coincidan.
solicitud.

Sesión Proporciona compatibilidad con la Antes de los componentes que


administración de sesiones de usuario. requieren Session.

Archivos estáticos Proporciona compatibilidad con la Terminal si hay una solicitud que
proporción de archivos estáticos y la coincida con archivos.
exploración de directorios.

Reescritura de direcciones URL Proporciona compatibilidad con la Antes de los componentes que
reescritura de direcciones URL y la consumen la dirección URL.
redirección de solicitudes.

WebSockets Habilita el protocolo WebSockets. Antes de los componentes necesarios


para aceptar solicitudes de WebSocket.

Escritura de middleware
El middleware normalmente está encapsulado en una clase y se expone con un método de extensión. Use el
siguiente middleware a modo de ejemplo. En este se establece la referencia cultural de la solicitud actual a partir de
la cadena de solicitud:

public class Startup


{
public void Configure(IApplicationBuilder app)
{
app.Use((context, next) =>
{
var cultureQuery = context.Request.Query["culture"];
if (!string.IsNullOrWhiteSpace(cultureQuery))
{
var culture = new CultureInfo(cultureQuery);

CultureInfo.CurrentCulture = culture;
CultureInfo.CurrentUICulture = culture;
}

// Call the next delegate/middleware in the pipeline


return next();
});

app.Run(async (context) =>


{
await context.Response.WriteAsync(
$"Hello {CultureInfo.CurrentCulture.DisplayName}");
});

}
}

Nota: El código de ejemplo anterior se usa para mostrar la creación de un componente de middleware. Vea
Globalization and localization (Globalización y localización) para ver la compatibilidad con localización integrada de
ASP.NET Core.
Puede probar el middleware pasando la referencia cultural, por ejemplo, http://localhost:7997/?culture=no .
El código siguiente mueve el delegado de middleware a una clase:

using Microsoft.AspNetCore.Http;
using System.Globalization;
using System.Threading.Tasks;

namespace Culture
{
public class RequestCultureMiddleware
{
private readonly RequestDelegate _next;

public RequestCultureMiddleware(RequestDelegate next)


{
_next = next;
}

public Task InvokeAsync(HttpContext context)


{
var cultureQuery = context.Request.Query["culture"];
if (!string.IsNullOrWhiteSpace(cultureQuery))
{
var culture = new CultureInfo(cultureQuery);

CultureInfo.CurrentCulture = culture;
CultureInfo.CurrentUICulture = culture;

// Call the next delegate/middleware in the pipeline


return this._next(context);
}
}
}

NOTE
En ASP.NET Core 1.x, el nombre del método del middleware Task debe ser Invoke . En ASP.NET Core 2.0 o posterior, el
nombre puede ser Invoke o InvokeAsync .

El método de extensión siguiente expone el middleware mediante IApplicationBuilder:

using Microsoft.AspNetCore.Builder;

namespace Culture
{
public static class RequestCultureMiddlewareExtensions
{
public static IApplicationBuilder UseRequestCulture(
this IApplicationBuilder builder)
{
return builder.UseMiddleware<RequestCultureMiddleware>();
}
}
}

El código siguiente llama al middleware desde Configure :


public class Startup
{
public void Configure(IApplicationBuilder app)
{
app.UseRequestCulture();

app.Run(async (context) =>


{
await context.Response.WriteAsync(
$"Hello {CultureInfo.CurrentCulture.DisplayName}");
});

}
}

El middleware debería seguir el principio de dependencias explicitas mediante la exposición de sus dependencias
en el constructor. El middleware se construye una vez por duración de la aplicación. Vea Dependencias bajo
solicitud a continuación si necesita compartir servicios con middleware en una solicitud.
Los componentes de middleware pueden resolver las dependencias de una inserción de dependencias mediante
parámetros del constructor. UseMiddleware<T> también puede aceptar parámetros adicionales directamente.
Dependencias bajo solicitud
Dado que el middleware se construye al inicio de la aplicación y no bajo solicitud, los servicios de duración con
ámbito que usan los constructores de middleware no se comparten con otros tipos insertados mediante
dependencias durante cada solicitud. Si debe compartir un servicio con ámbito entre su middleware y otros tipos,
agregue esos servicios a la signatura del método Invoke . El método Invoke puede aceptar parámetros adicionales
que propaga la inserción de dependencias. Por ejemplo:

public class MyMiddleware


{
private readonly RequestDelegate _next;

public MyMiddleware(RequestDelegate next)


{
_next = next;
}

public async Task Invoke(HttpContext httpContext, IMyScopedService svc)


{
svc.MyProperty = 1000;
await _next(httpContext);
}
}

Recursos adicionales
Migración de módulos HTTP a middleware
Inicio de aplicaciones
Solicitud de características
Factory-based middleware activation (Activación de middleware basada en Factory)
Activación de middleware con un contenedor de terceros
Middleware de ASP.NET Core
21/06/2018 • 19 minutes to read • Edit Online

Por Rick Anderson y Steve Smith


Vea o descargue el código de ejemplo (cómo descargarlo)

¿Qué es el middleware?
El middleware es un software que se ensambla a una canalización de una aplicación para controlar las
solicitudes y las respuestas. Cada componente puede hacer lo siguiente:
Elegir si se pasa la solicitud al siguiente componente de la canalización.
Realizar trabajos antes y después de invocar al siguiente componente de la canalización.
Los delegados de solicitudes se usan para crear la canalización de solicitudes. Estos también controlan
las solicitudes HTTP.
Los delegados de solicitudes se configuran con los métodos de extensión Run, Map y Use. Un delegado
de solicitudes se puede especificar en línea como un método anónimo (denominado middleware en
línea) o se puede definir en una clase reutilizable. Estas clases reutilizables y métodos anónimos en línea
son middleware o componentes de middleware. Cada componente de middleware de la canalización de
solicitudes es responsable de invocar al siguiente componente de la canalización o de cortocircuitar la
cadena en caso de ser necesario.
En Migración de módulos HTTP a middleware se explica la diferencia entre las canalizaciones de
solicitudes en ASP.NET Core y ASP.NET 4.x y se proporcionan más ejemplos de middleware.

Creación de una canalización de middleware con


IApplicationBuilder
La canalización de solicitudes de ASP.NET Core está compuesta por una secuencia de delegados de
solicitudes, a los que se llama uno después de otro, tal y como se muestra en este diagrama (el hilo de
ejecución sigue las flechas negras):
Cada delegado puede realizar operaciones antes y después del siguiente. También puede decidir no
pasar una solicitud al siguiente delegado, lo que se denomina "cortocircuitar" la canalización de
solicitudes. Este proceso es necesario muchas veces, ya que previene la realización de trabajo
innecesario. Por ejemplo, el middleware de archivos estáticos puede devolver una solicitud para un
archivo estático y cortocircuitar el resto de la canalización. Los delegados que controlan excepciones
deben llamarse al principio de la canalización para que puedan capturar las excepciones que se producen
en las fases siguientes de la canalización.
La aplicación ASP.NET Core más sencilla posible configura un solo delegado de solicitudes que controla
todas las solicitudes. En este caso no se incluye una canalización de solicitudes real. En su lugar, solo se
llama a una única función anónima en respuesta a todas las solicitudes HTTP.

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;

public class Startup


{
public void Configure(IApplicationBuilder app)
{
app.Run(async context =>
{
await context.Response.WriteAsync("Hello, World!");
});
}
}

El primer delegado app.Run finaliza la canalización.


Puede encadenar varios delegados de solicitudes con app.Use. El parámetro next representa el
siguiente delegado de la canalización. Recuerde que puede cortocircuitar la canalización si no llama al
siguiente parámetro. Normalmente puede realizar acciones antes y después del siguiente delegado, tal y
como se muestra en este ejemplo:

public class Startup


{
public void Configure(IApplicationBuilder app)
{
app.Use(async (context, next) =>
{
// Do work that doesn't write to the Response.
await next.Invoke();
// Do logging or other work that doesn't write to the Response.
});

app.Run(async context =>


{
await context.Response.WriteAsync("Hello from 2nd delegate.");
});
}
}
WARNING
No llame a next.Invoke después de haber enviado la respuesta al cliente. Se producirá una excepción si se
modifica HttpResponse después de haber iniciado la respuesta. Por ejemplo, se producirá una excepción al
realizar cambios como el establecimiento de encabezados, el código de estado, etc. Si escribe en el cuerpo de la
respuesta después de llamar a next :
Puede provocar una infracción del protocolo. Por ejemplo, si escribe más de la longitud content-length
establecida.
Puede dañar el formato del cuerpo. Por ejemplo, si escribe un pie de página en HTML en un archivo CSS.
HttpResponse.HasStarted es una sugerencia útil para indicar si se han enviado los encabezados o si se han
realizado escrituras en el cuerpo.

Ordenación
El orden en el que se agregan los componentes de middleware en el método Configure define el orden
en el que se invocarán en las solicitudes y el orden inverso de la respuesta. Este orden es esencial por
motivos de seguridad, rendimiento y funcionalidad.
El método Configure (que se muestra aquí) agrega los siguientes componentes de middleware:
1. Control de errores y excepciones
2. Servidor de archivos estáticos
3. Autenticación
4. MVC
ASP.NET Core 2.x
ASP.NET Core 1.x

public void Configure(IApplicationBuilder app)


{
app.UseExceptionHandler("/Home/Error"); // Call first to catch exceptions
// thrown in the following middleware.

app.UseStaticFiles(); // Return static files and end pipeline.

app.UseAuthentication(); // Authenticate before you access


// secure resources.

app.UseMvcWithDefaultRoute(); // Add MVC to the request pipeline.


}

En el código anterior, UseExceptionHandler es el primer componente de middleware que se agrega a la


canalización. Por tanto, captura todas las excepciones que se puedan producir en las llamadas
posteriores.
El middleware de archivos estáticos se llama al principio de la canalización para que pueda controlar
solicitudes y realizar cortocircuitos sin pasar por los componentes restantes. Este middleware no
proporciona comprobaciones de autorización. Los archivos que proporciona, incluidos los de wwwroot,
están disponibles de forma pública. Consulte Archivos estáticos para obtener más información sobre
cómo proteger este tipo de archivos.
ASP.NET Core 2.x
ASP.NET Core 1.x
Si el middleware de archivos estáticos no controla la solicitud, se pasa al middleware de identidad (
app.UseAuthentication ), que realiza la autenticación. Este middleware no cortocircuita las solicitudes sin
autenticación. Aunque autentique solicitudes, la autorización (y el rechazo) se producen después de que
MVC seleccione una página de Razor o un control y una acción concretos.
En el ejemplo siguiente se muestra un orden de middleware en el que el middleware de archivos
estáticos controla las solicitudes de archivos estáticos antes del middleware de compresión de
respuestas. Los archivos estáticos no se comprimen con este orden de middleware. Las respuestas MVC
de UseMvcWithDefaultRoute se pueden comprimir.

public void Configure(IApplicationBuilder app)


{
app.UseStaticFiles(); // Static files not compressed
// by middleware.
app.UseResponseCompression();
app.UseMvcWithDefaultRoute();
}

Use, Run y Map


Puede configurar la canalización HTTP con Use , Run y Map . El método Use puede cortocircuitar la
canalización (solo si no llama a un delegado de solicitudes next ). Run es una convención y es posible
que algunos componentes de middleware expongan métodos Run[Middleware] que se ejecutan al final
de la canalización.
Las extensiones Map* se usan como convenciones para la creación de ramas en la canalización. Map
crea una rama de la canalización de solicitudes según las coincidencias de la ruta de solicitud
proporcionada. Si la ruta de solicitud comienza con la ruta proporcionada, se ejecuta la creación de la
rama.

public class Startup


{
private static void HandleMapTest1(IApplicationBuilder app)
{
app.Run(async context =>
{
await context.Response.WriteAsync("Map Test 1");
});
}

private static void HandleMapTest2(IApplicationBuilder app)


{
app.Run(async context =>
{
await context.Response.WriteAsync("Map Test 2");
});
}

public void Configure(IApplicationBuilder app)


{
app.Map("/map1", HandleMapTest1);

app.Map("/map2", HandleMapTest2);

app.Run(async context =>


{
await context.Response.WriteAsync("Hello from non-Map delegate. <p>");
});
}
}

En la siguiente tabla se muestran las solicitudes y las respuestas de http://localhost:1234 con el código
anterior:

SOLICITUD RESPUESTA

localhost:1234 Saludos del delegado sin Map.

localhost:1234/map1 Prueba 1 de Map

localhost:1234/map2 Prueba 2 de Map

localhost:1234/map3 Saludos del delegado sin Map.

Cuando se usa , los segmentos de ruta que coincidan se eliminan de


Map HttpRequest.Path y se anexan a
HttpRequest.PathBase por cada solicitud.

MapWhen crea una rama de la canalización de solicitudes según el resultado del predicado
proporcionado. Se puede usar cualquier predicado de tipo Func<HttpContext, bool> para asignar
solicitudes a nuevas ramas de la canalización. En el ejemplo siguiente se usa un predicado para detectar
la presencia de una branch variable de cadena de consulta:

public class Startup


{
private static void HandleBranch(IApplicationBuilder app)
{
app.Run(async context =>
{
var branchVer = context.Request.Query["branch"];
await context.Response.WriteAsync($"Branch used = {branchVer}");
});
}

public void Configure(IApplicationBuilder app)


{
app.MapWhen(context => context.Request.Query.ContainsKey("branch"),
HandleBranch);

app.Run(async context =>


{
await context.Response.WriteAsync("Hello from non-Map delegate. <p>");
});
}
}

En la siguiente tabla se muestran las solicitudes y las respuestas de http://localhost:1234 con el código
anterior:

SOLICITUD RESPUESTA

localhost:1234 Saludos del delegado sin Map.

localhost:1234/?branch=master Rama usada = master

Map admite la anidación, por ejemplo:


app.Map("/level1", level1App => {
level1App.Map("/level2a", level2AApp => {
// "/level1/level2a"
//...
});
level1App.Map("/level2b", level2BApp => {
// "/level1/level2b"
//...
});
});

Map puede hacer coincidir varios segmentos a la vez, por ejemplo:

app.Map("/level1/level2", HandleMultiSeg);

Middleware integrado
ASP.NET Core incluye los siguientes componentes de middleware, así como una descripción del orden
en el que se deberían agregar:

SOFTWARE INTERMEDIO DESCRIPTION ORDEN

Autenticación Proporciona compatibilidad con Antes de que se necesite


autenticación. HttpContext.User . Terminal para
devoluciones de llamadas OAuth.

CORS Configura el uso compartido de Antes de los componentes que usan


recursos entre orígenes. CORS.

Diagnóstico Configura el diagnóstico. Antes de los componentes que


generan errores.

Encabezados reenviados Reenvía encabezados con proxy a la Antes de los componentes que
solicitud actual. consumen los campos actualizados
(ejemplos: esquema, host, cliente, IP
y método).

Invalidación del método HTTP Permite que una solicitud POST Antes de los componentes que
entrante invalide el método. consumen el método actualizado.

Redireccionamiento de HTTPS Redireccione todas las solicitudes Antes de los componentes que
HTTP a HTTPS (ASP.NET Core 2.1 o consumen la dirección URL.
posterior).

Seguridad de transporte estricta de Middleware de mejora de seguridad Antes de que se envíen las
HTTP (HSTS) que agrega un encabezado de respuestas y después de los
respuesta especial (ASP.NET Core 2.1 componentes que modifican las
o posterior). solicitudes (por ejemplo,
encabezados reenviados, reescritura
de URL).

Almacenamiento en caché de Proporciona compatibilidad con la Antes de los componentes que


respuestas captura de respuestas. requieren el almacenamiento en
caché.
SOFTWARE INTERMEDIO DESCRIPTION ORDEN

Compresión de respuesta Proporciona compatibilidad con la Antes de los componentes que


compresión de respuestas. requieren compresión.

Localización de solicitudes Proporciona compatibilidad con Antes de los componentes que


ubicación. dependen de la ubicación.

Enrutamiento Define y restringe las rutas de la Terminal para rutas que coincidan.
solicitud.

Sesión Proporciona compatibilidad con la Antes de los componentes que


administración de sesiones de requieren Session.
usuario.

Archivos estáticos Proporciona compatibilidad con la Terminal si hay una solicitud que
proporción de archivos estáticos y la coincida con archivos.
exploración de directorios.

Reescritura de direcciones URL Proporciona compatibilidad con la Antes de los componentes que
reescritura de direcciones URL y la consumen la dirección URL.
redirección de solicitudes.

WebSockets Habilita el protocolo WebSockets. Antes de los componentes


necesarios para aceptar solicitudes
de WebSocket.

Escritura de middleware
El middleware normalmente está encapsulado en una clase y se expone con un método de extensión.
Use el siguiente middleware a modo de ejemplo. En este se establece la referencia cultural de la solicitud
actual a partir de la cadena de solicitud:
public class Startup
{
public void Configure(IApplicationBuilder app)
{
app.Use((context, next) =>
{
var cultureQuery = context.Request.Query["culture"];
if (!string.IsNullOrWhiteSpace(cultureQuery))
{
var culture = new CultureInfo(cultureQuery);

CultureInfo.CurrentCulture = culture;
CultureInfo.CurrentUICulture = culture;
}

// Call the next delegate/middleware in the pipeline


return next();
});

app.Run(async (context) =>


{
await context.Response.WriteAsync(
$"Hello {CultureInfo.CurrentCulture.DisplayName}");
});

}
}

Nota: El código de ejemplo anterior se usa para mostrar la creación de un componente de middleware.
Vea Globalization and localization (Globalización y localización) para ver la compatibilidad con
localización integrada de ASP.NET Core.
Puede probar el middleware pasando la referencia cultural, por ejemplo,
http://localhost:7997/?culture=no .

El código siguiente mueve el delegado de middleware a una clase:


using Microsoft.AspNetCore.Http;
using System.Globalization;
using System.Threading.Tasks;

namespace Culture
{
public class RequestCultureMiddleware
{
private readonly RequestDelegate _next;

public RequestCultureMiddleware(RequestDelegate next)


{
_next = next;
}

public Task InvokeAsync(HttpContext context)


{
var cultureQuery = context.Request.Query["culture"];
if (!string.IsNullOrWhiteSpace(cultureQuery))
{
var culture = new CultureInfo(cultureQuery);

CultureInfo.CurrentCulture = culture;
CultureInfo.CurrentUICulture = culture;

// Call the next delegate/middleware in the pipeline


return this._next(context);
}
}
}

NOTE
En ASP.NET Core 1.x, el nombre del método del middleware Task debe ser Invoke . En ASP.NET Core 2.0 o
posterior, el nombre puede ser Invoke o InvokeAsync .

El método de extensión siguiente expone el middleware mediante IApplicationBuilder:

using Microsoft.AspNetCore.Builder;

namespace Culture
{
public static class RequestCultureMiddlewareExtensions
{
public static IApplicationBuilder UseRequestCulture(
this IApplicationBuilder builder)
{
return builder.UseMiddleware<RequestCultureMiddleware>();
}
}
}

El código siguiente llama al middleware desde Configure :


public class Startup
{
public void Configure(IApplicationBuilder app)
{
app.UseRequestCulture();

app.Run(async (context) =>


{
await context.Response.WriteAsync(
$"Hello {CultureInfo.CurrentCulture.DisplayName}");
});

}
}

El middleware debería seguir el principio de dependencias explicitas mediante la exposición de sus


dependencias en el constructor. El middleware se construye una vez por duración de la aplicación. Vea
Dependencias bajo solicitud a continuación si necesita compartir servicios con middleware en una
solicitud.
Los componentes de middleware pueden resolver las dependencias de una inserción de dependencias
mediante parámetros del constructor. UseMiddleware<T> también puede aceptar parámetros adicionales
directamente.
Dependencias bajo solicitud
Dado que el middleware se construye al inicio de la aplicación y no bajo solicitud, los servicios de
duración con ámbito que usan los constructores de middleware no se comparten con otros tipos
insertados mediante dependencias durante cada solicitud. Si debe compartir un servicio con ámbito
entre su middleware y otros tipos, agregue esos servicios a la signatura del método Invoke . El método
Invoke puede aceptar parámetros adicionales que propaga la inserción de dependencias. Por ejemplo:

public class MyMiddleware


{
private readonly RequestDelegate _next;

public MyMiddleware(RequestDelegate next)


{
_next = next;
}

public async Task Invoke(HttpContext httpContext, IMyScopedService svc)


{
svc.MyProperty = 1000;
await _next(httpContext);
}
}

Recursos adicionales
Migración de módulos HTTP a middleware
Inicio de aplicaciones
Solicitud de características
Factory-based middleware activation (Activación de middleware basada en Factory)
Activación de middleware con un contenedor de terceros
Activación de middleware basada en Factory en
ASP.NET Core
25/06/2018 • 4 minutes to read • Edit Online

Por Luke Latham


IMiddlewareFactory/IMiddleware es un punto de extensibilidad para la activación de middleware.
Los métodos de extensión UseMiddleware comprueban si un tipo registrado de middleware implementa
IMiddleware . Si es así, la instancia IMiddlewareFactory registrada en el contenedor se usa para resolver la
implementación IMiddleware en lugar de usar la lógica de activación de middleware basado en convenciones. El
middleware se registra como un servicio con ámbito o transitorio en el contenedor de servicios de la aplicación.
Ventajas:
Activación a petición (inyección de servicios con ámbito)
Tipado fuerte de middleware
IMiddleware se activa a petición, por lo que los servicios se pueden insertar en el constructor del middleware.
Vea o descargue el código de ejemplo (cómo descargarlo)
La aplicación de ejemplo muestra middleware activado por:
Convención. Para obtener más información sobre la activación de middleware convencional, consulte el tema
Middleware.
Una implementación de IMiddleware. La clase MiddlewareFactory predeterminada activa el middleware.
Las implementaciones de middleware funcionan de forma idéntica y registran el valor proporcionado por un
parámetro de cadena de consulta ( key ). El middleware usa un contexto de base de datos insertado (un servicio
con ámbito) para registrar el valor de cadena de consulta en una base de datos en memoria.

IMiddleware
IMiddleware define el middleware para la canalización de solicitudes de la aplicación. El método
InvokeAsync(HttpContext, RequestDelegate) controla las solicitudes y devuelve una Task que representa la
ejecución del middleware.
Middleware activado por convención:
public class ConventionalMiddleware
{
private readonly RequestDelegate _next;

public ConventionalMiddleware(RequestDelegate next)


{
_next = next;
}

public async Task InvokeAsync(HttpContext context, AppDbContext db)


{
var keyValue = context.Request.Query["key"];

if (!string.IsNullOrWhiteSpace(keyValue))
{
db.Add(new Request()
{
DT = DateTime.UtcNow,
MiddlewareActivation = "ConventionalMiddleware",
Value = keyValue
});

await db.SaveChangesAsync();
}

await _next(context);
}
}

Middleware activado por MiddlewareFactory :

public class IMiddlewareMiddleware : IMiddleware


{
private readonly AppDbContext _db;

public IMiddlewareMiddleware(AppDbContext db)


{
_db = db;
}

public async Task InvokeAsync(HttpContext context, RequestDelegate next)


{
var keyValue = context.Request.Query["key"];

if (!string.IsNullOrWhiteSpace(keyValue))
{
_db.Add(new Request()
{
DT = DateTime.UtcNow,
MiddlewareActivation = "IMiddlewareMiddleware",
Value = keyValue
});

await _db.SaveChangesAsync();
}

await next(context);
}
}

Las extensiones se crean para los middlewares:


public static class MiddlewareExtensions
{
public static IApplicationBuilder UseConventionalMiddleware(
this IApplicationBuilder builder)
{
return builder.UseMiddleware<ConventionalMiddleware>();
}

public static IApplicationBuilder UseIMiddlewareMiddleware(


this IApplicationBuilder builder)
{
return builder.UseMiddleware<IMiddlewareMiddleware>();
}
}

No se pueden pasar objetos al middleware activado por Factory con UseMiddleware :

public static IApplicationBuilder UseIMiddlewareMiddleware(


this IApplicationBuilder builder, bool option)
{
// Passing 'option' as an argument throws a NotSupportedException at runtime.
return builder.UseMiddleware<IMiddlewareMiddleware>(option);
}

El middleware activado por Factory se agrega al contenedor integrado en Startup.cs:

public void ConfigureServices(IServiceCollection services)


{
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});

services.AddDbContext<AppDbContext>(options =>
options.UseInMemoryDatabase("InMemoryDb"));

services.AddTransient<IMiddlewareMiddleware>();

services.AddMvc();
}

Ambos middlewares se registran en la canalización de procesamiento de solicitudes en Configure :


public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseConventionalMiddleware();
app.UseIMiddlewareMiddleware();

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseMvc();
}

IMiddlewareFactory
IMiddlewareFactory proporciona métodos para crear middleware. La implementación de Middleware Factory se
registra en el contenedor como un servicio con ámbito.
La implementación IMiddlewareFactory predeterminada, MiddlewareFactory, se encuentra en el paquete
Microsoft.AspNetCore.Http (fuente de referencia).

Recursos adicionales
Middleware
Activación de middleware con un contenedor de terceros
Activación de middleware con un contenedor de
terceros en ASP.NET Core
25/06/2018 • 4 minutes to read • Edit Online

Por Luke Latham


En este artículo se explica cómo usar IMiddlewareFactory e IMiddleware como un punto de extensibilidad para la
activación de middleware con un contenedor de terceros. Para obtener información introductoria sobre
IMiddlewareFactory e IMiddleware , vea el tema Activación de middleware basada en Factory.

Vea o descargue el código de ejemplo (cómo descargarlo)


En la aplicación de ejemplo se muestra una activación de middleware por medio de una implementación de
IMiddlewareFactory , SimpleInjectorMiddlewareFactory . En el ejemplo se usa el contenedor de inserción de
dependencias Simple Injector.
La implementación de middleware del ejemplo registra el valor proporcionado por un parámetro de cadena de
consulta ( key ). El middleware usa un contexto de base de datos insertado (un servicio con ámbito) para registrar
el valor de cadena de consulta en una base de datos en memoria.

NOTE
En la aplicación de ejemplo se usa Simple Injector única y exclusivamente con fines de demostración. El uso de Simple
Injector no está avalado. Los métodos de activación de middleware descritos en la documentación de Simple Injector y los
problemas de GitHub está recomendado por los responsables de Simple Injector. Para más información, vea la
documentación de Simple Injector y el repositorio de GitHub de Simple Injector.

IMiddlewareFactory
IMiddlewareFactory proporciona métodos para crear middleware.
En la aplicación de ejemplo, se implementa un Middleware Factory para crear una instancia de
SimpleInjectorActivatedMiddleware . Ese Middleware Factory usa el contenedor de Simple Injector para resolver el
middleware:
public class SimpleInjectorMiddlewareFactory : IMiddlewareFactory
{
private readonly Container _container;

public SimpleInjectorMiddlewareFactory(Container container)


{
_container = container;
}

public IMiddleware Create(Type middlewareType)


{
return _container.GetInstance(middlewareType) as IMiddleware;
}

public void Release(IMiddleware middleware)


{
// The container is responsible for releasing resources.
}
}

IMiddleware
IMiddleware define el middleware para la canalización de solicitudes de la aplicación.
Middleware activado por una implementación de IMiddlewareFactory
(Middleware/SimpleInjectorActivatedMiddleware.cs):

public class SimpleInjectorActivatedMiddleware : IMiddleware


{
private readonly AppDbContext _db;

public SimpleInjectorActivatedMiddleware(AppDbContext db)


{
_db = db;
}

public async Task InvokeAsync(HttpContext context, RequestDelegate next)


{
var keyValue = context.Request.Query["key"];

if (!string.IsNullOrWhiteSpace(keyValue))
{
_db.Add(new Request()
{
DT = DateTime.UtcNow,
MiddlewareActivation = "SimpleInjectorActivatedMiddleware",
Value = keyValue
});

await _db.SaveChangesAsync();
}

await next(context);
}
}

Se crea una extensión para el middleware (Middleware/MiddlewareExtensions.cs):


public static class MiddlewareExtensions
{
public static IApplicationBuilder UseSimpleInjectorActivatedMiddleware(
this IApplicationBuilder builder)
{
return builder.UseMiddleware<SimpleInjectorActivatedMiddleware>();
}
}

Startup.ConfigureServices debe realizar varias tareas:


Configurar el contenedor de Simple Injector.
Registrar tanto el Factory como el Middleware.
Poner disponible el contexto de base de datos de la aplicación en el contenedor de Simple Injector para una
página de Razor.

public void ConfigureServices(IServiceCollection services)


{
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});

services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

// Replace the default middleware factory with the


// SimpleInjectorMiddlewareFactory.
services.AddTransient<IMiddlewareFactory>(_ =>
{
return new SimpleInjectorMiddlewareFactory(_container);
});

// Wrap ASP.NET requests in a Simple Injector execution


// context.
services.UseSimpleInjectorAspNetRequestScoping(_container);

// Provide the database context from the Simple


// Injector container whenever it's requested from
// the default service container.
services.AddScoped<AppDbContext>(provider =>
_container.GetInstance<AppDbContext>());

_container.Options.DefaultScopedLifestyle = new AsyncScopedLifestyle();

_container.Register<AppDbContext>(() =>
{
var optionsBuilder = new DbContextOptionsBuilder<DbContext>();
optionsBuilder.UseInMemoryDatabase("InMemoryDb");
return new AppDbContext(optionsBuilder.Options);
}, Lifestyle.Scoped);

_container.Register<SimpleInjectorActivatedMiddleware>();

_container.Verify();
}

El middleware se registra en la canalización de procesamiento de solicitudes en Startup.Configure :


public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseSimpleInjectorActivatedMiddleware();

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseMvc();
}

Recursos adicionales
Middleware
Factory-based middleware activation (Activación de middleware basada en Factory)
Repositorio de GitHub de Simple Injector
Documentación de Simple Injector
Archivos estáticos en ASP.NET Core
25/06/2018 • 15 minutes to read • Edit Online

Por Rick Anderson y Scott Addie


Los archivos estáticos, como HTML, CSS, imágenes y JavaScript, son activos que una aplicación de
ASP.NET Core proporciona directamente a los clientes. Se necesita alguna configuración para habilitar el
servicio de estos archivos.
Vea o descargue el código de ejemplo (cómo descargarlo)

Proporcionar archivos estáticos


Los archivos estáticos se almacenan en el directorio raíz de la Web del proyecto. El directorio
predeterminado es <content_root>/wwwroot, pero puede cambiarse a través del método UseWebRoot.
Vea Raíz del contenido y Raíz web para obtener más información.
El host de web de la aplicación debe tener conocimiento del directorio raíz del contenido.
ASP.NET Core 2.x
ASP.NET Core 1.x
El método WebHost.CreateDefaultBuilder establece la raíz de contenido en el directorio actual:

public class Program


{
public static void Main(string[] args)
{
BuildWebHost(args).Run();
}

public static IWebHost BuildWebHost(string[] args) =>


WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.Build();
}

Se puede acceder a los archivos estáticos a través de una ruta de acceso relativa a la raíz web. Por ejemplo,
la plantilla de proyecto Aplicación web contiene varias carpetas dentro de la carpeta wwwroot:
wwwroot
css
images
js
El formato de URI para acceder a un archivo en la subcarpeta images es
http://<dirección_servidor>/images/<nombre_archivo_imagen>. Por ejemplo,
http://localhost:9189/images/banner3.svg.
ASP.NET Core 2.x
ASP.NET Core 1.x
Si el destino es .NET Framework, agregue el paquete Microsoft.AspNetCore.StaticFiles al proyecto. Si el
destino es .NET Core, el metapaquete Microsoft.AspNetCore.All incluye este paquete.
Configure el middleware, que permite proporcionar archivos estáticos.
Proporcionar archivos dentro de la raíz web
Invoque el método UseStaticFiles dentro de Startup.Configure :

public void Configure(IApplicationBuilder app)


{
app.UseStaticFiles();
}

La sobrecarga del método sin parámetros UseStaticFiles marca los archivos en la raíz web como que se
pueden proporcionar. El siguiente marcado hace referencia a wwwroot/images/banner1.svg:

<img src="~/images/banner1.svg" alt="ASP.NET" class="img-responsive" />

Proporcionar archivos fuera de la raíz web


Considere una jerarquía de directorios en la que residen fuera de la raíz web los archivos estáticos que se
van a proporcionar:
wwwroot
css
images
js
MyStaticFiles
images
banner1.svg
Una solicitud puede acceder al archivo banner1.svg configurando el middleware de archivos estáticos
como se muestra a continuación:

public void Configure(IApplicationBuilder app)


{
app.UseStaticFiles(); // For the wwwroot folder

app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(
Path.Combine(Directory.GetCurrentDirectory(), "MyStaticFiles")),
RequestPath = "/StaticFiles"
});
}

En el código anterior, la jerarquía del directorio MyStaticFiles se expone públicamente a través del
segmento de URI StaticFiles. Una solicitud a http://<dirección_servidor>/StaticFiles/images/banner1.svg
proporciona el archivo banner1.svg.
El siguiente marcado hace referencia a MyStaticFiles/images/banner1.svg:

<img src="~/StaticFiles/images/banner1.svg" alt="ASP.NET" class="img-responsive" />

Establecer encabezados de respuesta HTTP


Se puede usar un objeto StaticFileOptions para establecer encabezados de respuesta HTTP. Además de
configurar el servicio de archivos estáticos desde la raíz web, el código siguiente establece el encabezado
Cache-Control :

public void Configure(IApplicationBuilder app)


{
app.UseStaticFiles(new StaticFileOptions
{
OnPrepareResponse = ctx =>
{
// Requires the following import:
// using Microsoft.AspNetCore.Http;
ctx.Context.Response.Headers.Append("Cache-Control", "public,max-age=600");
}
});
}

El método HeaderDictionaryExtensions.Append existe en el paquete Microsoft.AspNetCore.Http.


Los archivos se han hecho públicamente almacenables en caché durante 10 minutos (600 segundos):

Autorización de archivos estáticos


El middleware de archivos estáticos no proporciona comprobaciones de autorización. Los archivos que
proporciona, incluidos los de wwwroot, están accesibles de forma pública. Para proporcionar archivos
según su autorización:
Almacénelos fuera de wwwroot y cualquier directorio accesible por el middleware de archivos estáticos
y
Proporciónelos a través de un método de acción al que se aplica la autorización. Devuelva un objeto
FileResult:

[Authorize]
public IActionResult BannerImage()
{
var file = Path.Combine(Directory.GetCurrentDirectory(),
"MyStaticFiles", "images", "banner1.svg");

return PhysicalFile(file, "image/svg+xml");


}

Habilite el examen de directorios


El examen de directorios permite a los usuarios de su aplicación web ver una lista de directorios y archivos
contenidos en un directorio especificado. Por motivos de seguridad, el examen de directorios está
deshabilitado de forma predeterminada (consulte Consideraciones). Habilitar el examen de directorios
invocando el método UseDirectoryBrowser en Startup.Configure :
public void Configure(IApplicationBuilder app)
{
app.UseStaticFiles(); // For the wwwroot folder

app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(
Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "images")),
RequestPath = "/MyImages"
});

app.UseDirectoryBrowser(new DirectoryBrowserOptions
{
FileProvider = new PhysicalFileProvider(
Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "images")),
RequestPath = "/MyImages"
});
}

Agregar servicios requeridos invocando el método AddDirectoryBrowser desde


Startup.ConfigureServices :

public void ConfigureServices(IServiceCollection services)


{
services.AddDirectoryBrowser();
}

El código anterior permite el examen de directorios de la carpeta wwwroot/images usando la dirección


URL http://<dirección_servidor>/MyImages, con vínculos a cada archivo y carpeta:

Vea consideraciones sobre los riesgos de seguridad al habilitar el examen.


Tenga en cuenta las dos llamadas a UseStaticFiles en el ejemplo siguiente. La primera llamada permite
proporcionar archivos estáticos en la carpeta wwwroot. La segunda llamada habilita el examen de
directorios de la carpeta wwwroot/images usando la dirección URL
http://<dirección_servidor>/MyImages:
public void Configure(IApplicationBuilder app)
{
app.UseStaticFiles(); // For the wwwroot folder

app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(
Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "images")),
RequestPath = "/MyImages"
});

app.UseDirectoryBrowser(new DirectoryBrowserOptions
{
FileProvider = new PhysicalFileProvider(
Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "images")),
RequestPath = "/MyImages"
});
}

Proporcionar un documento predeterminado


Establecer una página principal predeterminada proporciona a los visitantes un punto de partida lógico
cuando visitan su sitio. Para proporcionar una página predeterminada sin que el usuario cumpla por
completo los requisitos del URI, llame al método UseDefaultFiles desde Startup.Configure :

public void Configure(IApplicationBuilder app)


{
app.UseDefaultFiles();
app.UseStaticFiles();
}

IMPORTANT
Debe llamarse a UseDefaultFiles antes de a UseStaticFiles para proporcionar el archivo predeterminado.
UseDefaultFiles es un sistema de reescritura de direcciones URL que no proporciona realmente el archivo.
Habilite el middleware de archivos estáticos a través de UseStaticFiles para proporcionar el archivo.

Con UseDefaultFiles , las solicitudes a una carpeta buscan:


default.htm
default.html
index.htm
index.html
El primer archivo que se encuentra en la lista se proporciona como si la solicitud fuera el URI completo. La
dirección URL del explorador sigue reflejando el URI solicitado.
El código siguiente cambia el nombre de archivo predeterminado a mydefault.html:
public void Configure(IApplicationBuilder app)
{
// Serve my app-specific default file, if present.
DefaultFilesOptions options = new DefaultFilesOptions();
options.DefaultFileNames.Clear();
options.DefaultFileNames.Add("mydefault.html");
app.UseDefaultFiles(options);
app.UseStaticFiles();
}

UseFileServer
UseFileServer combina la funcionalidad de UseStaticFiles , UseDefaultFiles y UseDirectoryBrowser .
El código siguiente permite proporcionar archivos estáticos y el archivo predeterminado. El examen de
directorios no está habilitado.

app.UseFileServer();

El código siguiente refuerza la sobrecarga sin parámetros habilitando el examen de directorios:

app.UseFileServer(enableDirectoryBrowsing: true);

Tenga en cuenta la siguiente jerarquía de directorios:


wwwroot
css
images
js
MyStaticFiles
images
banner1.svg
default.html
El código siguiente permite los archivos estáticos, los archivos predeterminados y el examen de directorios
de MyStaticFiles :

public void Configure(IApplicationBuilder app)


{
app.UseStaticFiles(); // For the wwwroot folder

app.UseFileServer(new FileServerOptions
{
FileProvider = new PhysicalFileProvider(
Path.Combine(Directory.GetCurrentDirectory(), "MyStaticFiles")),
RequestPath = "/StaticFiles",
EnableDirectoryBrowsing = true
});
}

Se debe llamar a AddDirectoryBrowser cuando el valor de la propiedad EnableDirectoryBrowsing es true :


public void ConfigureServices(IServiceCollection services)
{
services.AddDirectoryBrowser();
}

Al usar la jerarquía de archivos y el código anterior, las direcciones URL se resuelven como se indica a
continuación:

IDENTIFICADOR URI RESPUESTA

http://<dirección_servidor>/StaticFiles/images/banner1 MyStaticFiles/images/banner1.svg
.svg

http://<dirección_servidor>/StaticFiles MyStaticFiles/default.html

Si no existe ningún archivo con el nombre predeterminado en el directorio MyStaticFiles,


http://<dirección_servidor>/StaticFiles devuelve la lista de directorios con vínculos activos:

NOTE
UseDefaultFiles y UseDirectoryBrowser usan la dirección URL http://<dirección_servidor>/StaticFiles sin la
barra diagonal final para desencadenar un redireccionamiento del lado cliente a
http://<dirección_servidor>/StaticFiles/. Tenga en cuenta la adición de la barra diagonal final. Las direcciones URL
relativas dentro de los documentos se consideran no válidas sin una barra diagonal final.

FileExtensionContentTypeProvider
La clase FileExtensionContentTypeProvider contiene una propiedad Mappings que actúa como una
asignación de extensiones de archivo para tipos de contenido MIME. En el ejemplo siguiente, se registran
varias extensiones de archivo a los tipos MIME conocidos. Se reemplaza la extensión .rtf y se quita .mp4.
public void Configure(IApplicationBuilder app)
{
// Set up custom content types - associating file extension to MIME type
var provider = new FileExtensionContentTypeProvider();
// Add new mappings
provider.Mappings[".myapp"] = "application/x-msdownload";
provider.Mappings[".htm3"] = "text/html";
provider.Mappings[".image"] = "image/png";
// Replace an existing mapping
provider.Mappings[".rtf"] = "application/x-msdownload";
// Remove MP4 videos.
provider.Mappings.Remove(".mp4");

app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(
Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "images")),
RequestPath = "/MyImages",
ContentTypeProvider = provider
});

app.UseDirectoryBrowser(new DirectoryBrowserOptions
{
FileProvider = new PhysicalFileProvider(
Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "images")),
RequestPath = "/MyImages"
});
}

Vea Tipos de contenido MIME.

Tipos de contenido no estándar


El middleware de archivos estáticos entiende casi 400 tipos de contenido de archivo conocidos. Si el
usuario solicita un archivo de un tipo de archivo desconocido, el middleware de archivos estáticos
devuelve una respuesta HTTP 404 (No encontrado). Si se habilita el examen de directorios, se muestra un
vínculo al archivo. El URI devuelve un error HTTP 404.
El código siguiente permite proporcionar tipos desconocidos y procesa el archivo desconocido como una
imagen:

public void Configure(IApplicationBuilder app)


{
app.UseStaticFiles(new StaticFileOptions
{
ServeUnknownFileTypes = true,
DefaultContentType = "image/png"
});
}

Con el código anterior, una solicitud para un archivo con un tipo de contenido desconocido se devuelve
como una imagen.

WARNING
Habilitar ServeUnknownFileTypes supone un riesgo para la seguridad. Está deshabilitado de forma predeterminada
y no se recomienda su uso. FileExtensionContentTypeProvider proporciona una alternativa más segura a ofrecer
archivos con extensiones no estándar.

Consideraciones
WARNING
UseDirectoryBrowser y UseStaticFiles pueden producir pérdidas de información confidencial. Se recomienda
deshabilitar el examen de directorios en producción. Revise cuidadosamente los directorios que se habilitan
mediante UseStaticFiles o UseDirectoryBrowser . Todo el directorio y sus subdirectorios pasan a ser accesibles
públicamente. Almacene los archivos adecuados para proporcionarlos al público en un directorio dedicado, como
<raíz_contenido>/wwwroot. Separe estos archivos de las vistas MVC, las páginas de Razor (solo 2.x), los archivos
de configuración, etc.

Las direcciones URL para el contenido que se expone a través de UseDirectoryBrowser y


UseStaticFiles están sujetas a la distinción entre mayúsculas y minúsculas, y a restricciones de
caracteres del sistema de archivos subyacente. Por ejemplo, Windows no distingue entre
mayúsculas y minúsculas, pero macOS y Linux sí.
Las aplicaciones de ASP.NET Core hospedadas en IIS usan el módulo de ASP.NET Core para
reenviar todas las solicitudes a la aplicación, incluidas las solicitudes de archivos estáticos. No se
usa el controlador de archivos estáticos de IIS. No tiene ninguna posibilidad de controlar las
solicitudes antes de que las controle el módulo.
Complete los pasos siguientes en el Administrador de IIS para quitar el controlador de archivos
estáticos de IIS en el nivel de servidor o de sitio web:
1. Navegue hasta la característica Módulos.
2. En la lista, seleccione StaticFileModule.
3. Haga clic en Quitar en la barra lateral Acciones.

WARNING
Si el controlador de archivos estáticos de IIS está habilitado y el módulo de ASP.NET Core no está configurado
correctamente, se proporcionan archivos estáticos. Esto sucede, por ejemplo, si el archivo web.config no está
implementado.

Coloque los archivos de código (incluidos .cs y .cshtml) fuera de la raíz web del proyecto de la
aplicación. Por lo tanto, se crea una separación lógica entre el contenido del lado cliente de la aplicación
y el código basado en servidor. Esto impide que se filtre el código del lado servidor.

Recursos adicionales
Middleware
Introducción a ASP.NET Core
Enrutamiento en ASP.NET Core
18/06/2018 • 41 minutes to read • Edit Online

Por Ryan Nowak, Steve Smith y Rick Anderson


La funcionalidad de enrutamiento de ASP.NET Core se encarga de asignar una solicitud entrante a un
controlador de ruta. Las rutas se definen en la aplicación ASP.NET y se configuran cuando se inicia la
aplicación. Una ruta puede extraer opcionalmente valores de la dirección URL contenida en la solicitud, que
se pueden usar para procesar las solicitudes. Con la información de ruta de la aplicación ASP.NET, la
funcionalidad de enrutamiento también puede generar direcciones URL que se asignan a controladores de
ruta. Por tanto, el enrutamiento puede buscar un controlador de ruta basado en una dirección URL o la
dirección URL correspondiente a un controlador de ruta determinado en función de la información del
controlador de ruta.

IMPORTANT
En este documento se describe el enrutamiento de nivel bajo de ASP.NET Core. Para más información sobre el
enrutamiento de ASP.NET Core MVC, vea Enrutamiento a las acciones de controlador.

Vea o descargue el código de ejemplo (cómo descargarlo)

Fundamentos del enrutamiento


El enrutamiento usa rutas (implementaciones de IRouter) para:
asignar las solicitudes entrantes a controladores de ruta
generar direcciones URL que se usan en las respuestas
Por lo general, una aplicación tiene una sola colección de rutas. Cuando llega una solicitud, la colección de
rutas se procesa en orden. La solicitud entrante busca una ruta que coincida con la dirección URL de la
solicitud. Para ello, llama al método RouteAsync en cada ruta disponible de la colección de rutas. En cambio,
una respuesta puede usar el enrutamiento para generar direcciones URL (por ejemplo, para el
redireccionamiento o los vínculos) en función de la información de ruta. De este modo, evita tener que
codificar de forma rígida las direcciones URL, lo que facilita el mantenimiento.
La clase RouterMiddleware conecta el enrutamiento a la canalización de software intermedio. ASP.NET Core
MVC agrega enrutamiento a la canalización de software intermedio como parte de su configuración. Para
obtener información sobre el uso del enrutamiento como componente independiente, vea Uso de software
intermedio de enrutamiento.
Coincidencia de dirección URL
La coincidencia de dirección URL es el proceso por el cual el enrutamiento envía una solicitud entrante a un
controlador. Este proceso suele basarse en datos de la ruta de dirección URL, pero se puede ampliar para
tener en cuenta los datos de la solicitud. La capacidad de enviar solicitudes a controladores independientes
es clave para escalar el tamaño y la complejidad de una aplicación.
Las solicitudes entrantes especifican la clase RouterMiddleware , que llama al método RouteAsync en cada
ruta de la secuencia. La instancia de IRouter decide si controla la solicitud mediante el establecimiento de
RouteContext.Handler en un RequestDelegate no nulo. Si una ruta establece un controlador para la
solicitud, el procesamiento de rutas se detiene y se invoca el controlador para procesar la solicitud. Si se
prueban todas las rutas y no se encuentra ningún controlador para la solicitud, el software intermedio llama
al siguiente y se invoca el siguiente software intermedio de la canalización de solicitudes.
La entrada principal a RouteAsync es el RouteContext.HttpContext asociado a la solicitud actual.
RouteContext.Handler y RouteContext.RouteData son salidas que se establecerán cuando una ruta coincida.
Una coincidencia durante RouteAsync también establecerá las propiedades de RouteContext.RouteData en
los valores adecuados en función del procesamiento de solicitudes realizado hasta el momento. Si una ruta
coincide con una solicitud, RouteContext.RouteData contendrá información de estado importante sobre el
resultado.
RouteData.Values es un diccionario con los valores de ruta generados desde la ruta. Estos valores suelen
determinarse mediante la conversión en tokens de la dirección URL, y pueden usarse para aceptar la
entrada del usuario o para tomar otras decisiones sobre el envío dentro de la aplicación.
RouteData.DataTokens es un contenedor de propiedades de datos adicionales relacionados con la ruta
coincidente. Se proporcionan DataTokens para permitir la asociación de datos de estado con cada ruta, de
modo que la aplicación pueda tomar decisiones más adelante en función de las rutas que han coincidido.
Estos valores los define el desarrollador y no afectan de ninguna manera al comportamiento del
enrutamiento. Además, los valores que se guardan provisionalmente en tokens de datos pueden ser de
cualquier tipo, a diferencia de los valores de ruta, que deben poder convertirse fácilmente en cadenas y a
partir de estas.
RouteData.Routers es una lista de las rutas que han participado en encontrar una coincidencia correcta con
la solicitud. Las rutas se pueden anidar unas dentro de otras y la propiedad Routers refleja la ruta de
acceso del árbol lógico de rutas que han tenido como resultado una coincidencia. Por lo general, el primer
elemento de Routers es la colección de rutas y se debe usar para la generación de direcciones URL. El
último elemento de Routers es el controlador de ruta que ha coincidido.
Generación de dirección URL
La generación de dirección URL es el proceso por el cual el enrutamiento puede crear una ruta de dirección
URL basada en un conjunto de valores de ruta. Esto permite una separación lógica entre los controladores
y las direcciones URL que tienen acceso a ellos.
La generación de dirección URL sigue un proceso iterativo similar, pero se inicia cuando el código de
usuario o de marco de trabajo llama al método GetVirtualPath de la colección de rutas. Se llamará en
secuencia al método GetVirtualPath de cada ruta hasta que se devuelva un valor VirtualPathData no nulo.
La principal entradas de GetVirtualPath son:
VirtualPathContext.HttpContext

VirtualPathContext.Values

VirtualPathContext.AmbientValues

Las rutas usan principalmente los valores de ruta proporcionados por Values y AmbientValues para decidir
dónde se puede generar una dirección URL y qué valores se deben incluir. AmbientValues son el conjunto
de valores de ruta producidos cuando la solicitud actual coincide con el sistema de enrutamiento. En
cambio, Values son los valores de ruta que especifican cómo se genera la dirección URL deseada para la
operación actual. Se proporciona HttpContext por si una ruta necesita obtener servicios o datos
adicionales asociados con el contexto actual.
Sugerencia: Considere Values como un conjunto de invalidaciones de AmbientValues . La generación de
dirección URL intenta reutilizar los valores de ruta de la solicitud actual para que sea más fácil generar
direcciones URL para los vínculos con la misma ruta o valores de ruta.
La salida de GetVirtualPath es VirtualPathData . VirtualPathData es un valor paralelo de RouteData ;
contiene el valor VirtualPath de la dirección URL de salida y algunas propiedades más que la ruta debe
establecer.
La propiedad VirtualPathData.VirtualPath contiene la ruta de acceso virtual generada por la ruta. Es
posible que deba procesar aún más la ruta de acceso, según sus necesidades. Por ejemplo, si quiere
representar la dirección URL generada en HTML debe anteponer la ruta de acceso base de la aplicación.
VirtualPathData.Router es una referencia a la ruta que ha generado correctamente la dirección URL.
La propiedad VirtualPathData.DataTokens es un diccionario de datos adicionales relacionados con la ruta
que ha generado la dirección URL. Se trata del valor paralelo de RouteData.DataTokens .
Creación de rutas
El enrutamiento proporciona la clase Route como implementación estándar de IRouter . Route usa la
sintaxis de plantilla de ruta para definir los patrones que se harán coincidir con la ruta de dirección URL
cuando se llame a RouteAsync . Route usará la misma plantilla de ruta para generar una dirección URL
cuando se llame a GetVirtualPath .
La mayoría de las aplicaciones creará rutas mediante una llamada a MapRoute o a uno de los métodos de
extensión similares definidos en IRouteBuilder . Todos estos métodos crearán una instancia de Route y la
agregarán a la colección de rutas.
Nota: MapRoute no toma ningún parámetro de controlador de ruta, solo agrega las rutas que procesará
DefaultHandler . Dado que el controlador predeterminado es IRouter , es posible que decida no controlar
la solicitud. Por ejemplo, ASP.NET MVC suele configurarse como un controlador predeterminado que solo
controla las solicitudes que coinciden con un controlador y una acción disponibles. Para más información
sobre el enrutamiento a MVC, vea Enrutamiento a las acciones de controlador.
Este es un ejemplo de una llamada a MapRoute usada por una definición de ruta típica de ASP.NET MVC:

routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");

Esta plantilla coincidirá con una ruta de dirección URL como /Products/Details/17 y extraerá los valores de
ruta { controller = Products, action = Details, id = 17 } . Los valores de ruta se determinan al dividir la
ruta de dirección URL en segmentos y hacer coincidir cada segmento con el nombre del parámetro de ruta
de la plantilla de ruta. Los parámetros de ruta tienen un nombre asignado. Para definirlos, el nombre del
parámetro se incluye entre llaves { } .
La plantilla anterior también podría coincidir con la ruta de dirección URL / y generaría los valores
{ controller = Home, action = Index } . Esto se debe a que los parámetros de ruta {controller} y
{action} tienen valores predeterminados y el parámetro de ruta id es opcional. Un signo igual =
seguido de un valor después del nombre del parámetro de ruta define un valor predeterminado para el
parámetro. Un signo de interrogación ? después del nombre del parámetro de ruta define el parámetro
como opcional. Los parámetros de ruta con un valor predeterminado siempre generan un valor de ruta
cuando la ruta coincide, mientras que los parámetros opcionales no generarán un valor de ruta si no hay
ningún segmento de ruta de dirección URL correspondiente.
Vea en Referencia de plantilla de ruta una descripción detallada de las características y la sintaxis de la
plantilla de ruta.
En este ejemplo se incluye una restricción de ruta:
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id:int}");

Esta plantilla coincidirá con una ruta de dirección URL como /Products/Details/17 , pero no con
/Products/Details/Apples . La definición de parámetro de ruta {id:int} define una restricción de ruta para
el parámetro de ruta id . Las restricciones de ruta implementan IRouteConstraint e inspeccionan los
valores de ruta para comprobarlos. En este ejemplo, el valor de ruta id debe poder convertirse en un
entero. Vea en Referencia de restricción de ruta una explicación más detallada de las restricciones de ruta
que se proporcionan con el marco de trabajo.
Las sobrecargas adicionales de MapRoute aceptan valores para constraints , dataTokens y defaults . Estos
parámetros adicionales de MapRoute se definen como un tipo object . Estos parámetros suelen usarse para
pasar un objeto de tipo anónimo, donde los nombres de propiedad del tipo anónimo coinciden con los
nombres de los parámetros de ruta.
En los dos ejemplos siguientes se crean rutas equivalentes:

routes.MapRoute(
name: "default_route",
template: "{controller}/{action}/{id?}",
defaults: new { controller = "Home", action = "Index" });

routes.MapRoute(
name: "default_route",
template: "{controller=Home}/{action=Index}/{id?}");

Sugerencia: La sintaxis insertada para definir las restricciones y los valores predeterminados puede ser más
conveniente para rutas simples, pero hay algunas características como los tokens de datos que no son
compatibles con esta sintaxis.
En este ejemplo se muestran otras características:

routes.MapRoute(
name: "blog",
template: "Blog/{*article}",
defaults: new { controller = "Blog", action = "ReadArticle" });

Esta plantilla coincidirá con una ruta de dirección URL como /Blog/All-About-Routing/Introduction y
extraerá los valores
{ controller = Blog, action = ReadArticle, article = All-About-Routing/Introduction } . La ruta genera los
valores de ruta predeterminados para controller y action , incluso si no hay parámetros de ruta
correspondientes en la plantilla. Los valores predeterminados pueden especificarse en la plantilla de ruta. El
parámetro de ruta article se define como un comodín por la aparición de un asterisco * antes del
nombre del parámetro de ruta. Los parámetros de ruta comodín capturan el resto de la ruta de dirección
URL y también pueden coincidir con la cadena vacía.
En este ejemplo se agregan restricciones de ruta y tokens de datos:

routes.MapRoute(
name: "us_english_products",
template: "en-US/Products/{id}",
defaults: new { controller = "Products", action = "Details" },
constraints: new { id = new IntRouteConstraint() },
dataTokens: new { locale = "en-US" });
Esta plantilla coincidirá con una ruta de dirección URL como /Products/5 y extraerá los valores
{ controller = Products, action = Details, id = 5 } y los tokens de datos { locale = en-US } .

Generación de dirección URL


La clase Route también puede llevar a cabo la generación de dirección URL mediante la combinación de
un conjunto de valores de ruta con su plantilla de ruta. Se trata lógicamente del proceso inverso de hacer
coincidir la ruta de dirección URL.
Sugerencia: Para entender mejor la generación de dirección URL, imagine qué dirección URL quiere
generar y, después, piense cómo coincidiría una plantilla de ruta con esa dirección URL. ¿Qué valores se
generarían? Este es un equivalente aproximado de cómo funciona la generación de dirección URL en la
clase Route .
En este ejemplo se usa una ruta de estilo básica de ASP.NET MVC:

routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");

Con los valores de ruta { controller = Products, action = List } , esta ruta generará la dirección URL
/Products/List . Los valores de ruta se sustituyen por los parámetros de ruta correspondientes para formar
la ruta de dirección URL. Dado que id es un parámetro de ruta opcional, no supone ningún problema que
no tenga un valor.
Con los valores de ruta { controller = Home, action = Index } , esta ruta generará la dirección URL / . Los
valores de ruta proporcionados coinciden con los valores predeterminados, por lo que los segmentos
correspondientes con esos valores se pueden omitir sin ningún riesgo. Tenga en cuenta que ambas
direcciones URL generadas harían un recorrido de ida y vuelta con esta definición de ruta y generarían los
mismos valores de ruta que se han usado para generar la dirección URL.
Sugerencia: Las aplicaciones con ASP.NET MVC deben usar UrlHelper para generar direcciones URL en
lugar de llamar al enrutamiento directamente.
Para obtener más detalles sobre el proceso de generación de dirección URL, vea Referencia de generación
de dirección URL.

Uso de software intermedio de enrutamiento


Agregue el paquete NuGet "Microsoft.AspNetCore.Routing".
Agregue enrutamiento al contenedor de servicios en Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddRouting();
}

Las rutas deben configurarse en el método Configure en la clase Startup . En el ejemplo siguiente se usan
estas API:
RouteBuilder
Build
MapGet coincide solamente con solicitudes HTTP GET
UseRouter

public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)


{
var trackPackageRouteHandler = new RouteHandler(context =>
{
var routeValues = context.GetRouteData().Values;
return context.Response.WriteAsync(
$"Hello! Route values: {string.Join(", ", routeValues)}");
});

var routeBuilder = new RouteBuilder(app, trackPackageRouteHandler);

routeBuilder.MapRoute(
"Track Package Route",
"package/{operation:regex(^(track|create|detonate)$)}/{id:int}");

routeBuilder.MapGet("hello/{name}", context =>


{
var name = context.GetRouteValue("name");
// This is the route handler when HTTP GET "hello/<anything>" matches
// To match HTTP GET "hello/<anything>/<anything>,
// use routeBuilder.MapGet("hello/{*name}"
return context.Response.WriteAsync($"Hi, {name}!");
});

var routes = routeBuilder.Build();


app.UseRouter(routes);
}

En la tabla siguiente se muestran las respuestas con los URI especificados.

IDENTIFICADOR URI RESPUESTA

/package/create/3 Hello! Valores de ruta: [operation, create], [id, 3]

/package/track/-3 Hello! Valores de ruta: [operation, track], [id, -3]

/package/track/-3/ Hello! Valores de ruta: [operation, track], [id, -3]

/package/track/ <Pasar explícitamente, ninguna coincidencia>

GET /hello/Joe Hi, Joe!

POST /hello/Joe <Pasar explícitamente, solo coincide con HTTP GET>


IDENTIFICADOR URI RESPUESTA

GET /hello/Joe/Smith <Pasar explícitamente, ninguna coincidencia>

Si va a configurar una única ruta, pase una instancia de IRouter para llamar a app.UseRouter . No tendrá
que llamar a RouteBuilder .
El marco de trabajo proporciona un conjunto de métodos de extensión para crear rutas, como los
siguientes:
MapRoute
MapGet
MapPost
MapPut
MapDelete
MapVerb

Algunos de estos métodos, como MapGet , requieren que se proporcione un valor RequestDelegate . El valor
RequestDelegate se usará como controlador de ruta cuando la ruta coincida. Otros métodos de esta familia
permiten configurar una canalización de software intermedio que se usará como controlador de ruta. Si el
método Map no acepta un controlador, como MapRoute , usará DefaultHandler .
Los métodos Map[Verb] usan restricciones para limitar la ruta al verbo HTTP en el nombre del método. Por
ejemplo, vea MapGet y MapVerb.

Referencia de plantilla de ruta


Los tokens entre llaves ( { } ) definen parámetros de ruta que se enlazarán si se encuentran coincidencias
con la ruta. Puede definir más de un parámetro de ruta en un segmento de ruta, pero deben estar
separados por un valor literal. Por ejemplo, {controller=Home}{action=Index} no sería una ruta válida, ya
que no hay ningún valor literal entre {controller} y {action} . Estos parámetros de ruta deben tener un
nombre y, opcionalmente, atributos adicionales especificados.
El texto literal diferente de los parámetros de ruta (por ejemplo, {id} ) y el separador de ruta / deben
coincidir con el texto de la dirección URL. La coincidencia de texto no distingue mayúsculas de minúsculas y
se basa en la representación descodificada de la ruta de las direcciones URL. Para que el delimitador de
parámetro de ruta literal { o } coincida, repita el carácter ( {{ o }} ) para usarlo como secuencia de
escape.
Es necesario tener en cuenta otras consideraciones en el caso de los patrones de dirección URL que
intentan capturar un nombre de archivo con una extensión de archivo opcional. Por ejemplo, cuando se usa
la plantilla files/{filename}.{ext?} , si existen filename y ext se rellenarán ambos valores. Si solo existe
filename en la dirección URL, la ruta coincide porque el punto final . es opcional. Las direcciones URL
siguientes coincidirían con esta ruta:
/files/myFile.txt
/files/myFile

Puede usar el carácter * como prefijo de un parámetro de ruta para enlazar con el resto del URI; es lo que
se denomina un parámetro comodín. Por ejemplo, blog/{*slug} coincidirá con cualquier URI que empiece
por /blog y que vaya seguido por cualquier valor (que se asignaría al valor de ruta slug ). Los parámetros
comodín también pueden coincidir con una cadena vacía.
Los parámetros de ruta pueden tener valores predeterminados. Para designar un valor predeterminado, se
especifica después del nombre de parámetro, separado por un signo = . Por ejemplo, {controller=Home}
definiría Home como el valor predeterminado de controller . El valor predeterminado se usa si no hay
ningún valor en la dirección URL para el parámetro. Además de los valores predeterminados, los
parámetros de ruta pueden ser opcionales (para especificarlos, se anexa un signo ? al final del nombre del
parámetro, como en id? ). La diferencia entre ser opcional y tener un valor predeterminado es que un
parámetro de ruta con un valor predeterminado siempre genera un valor, mientras que un parámetro
opcional tiene un valor solo cuando se proporciona uno.
Los parámetros de ruta también pueden tener restricciones, que deben coincidir con el valor de ruta
enlazado desde la dirección URL. Si se agrega un carácter de dos puntos : y un nombre de restricción
después del nombre del parámetro de ruta se especifica una restricción insertada en un parámetro de ruta.
Si la restricción requiere argumentos, se proporcionan entre paréntesis ( ) después del nombre de
restricción. Se pueden especificar varias restricciones insertadas si se anexa otro carácter de dos puntos :
y un nombre de restricción. El nombre de restricción se pasa al servicio IInlineConstraintResolver para
crear una instancia de IRouteConstraint para su uso en el procesamiento de direcciones URL. Por ejemplo,
la plantilla de ruta blog/{article:minlength(10)} especifica la restricción minlength con el argumento 10 .
Para obtener una descripción más detalladas de las restricciones de ruta y una lista de las restricciones
proporcionadas por el marco de trabajo, vea Referencia de restricción de ruta.
En la tabla siguiente se muestran algunas plantillas de ruta y su comportamiento.

DIRECCIÓN URL COINCIDENTE DE


PLANTILLA DE RUTA EJEMPLO NOTAS

hello /hello Solo coincide con la ruta de acceso


única /hello

{Page=Home} / Coincide y establece Page en


Home

{Page=Home} /Contact Coincide y establece Page en


Contact

{controller}/{action}/{id?} /Products/List Se asigna al controlador Products y


la acción List

{controller}/{action}/{id?} /Products/Details/123 Se asigna al controlador Products y


la acción Details . id se establece
en 123

{controller=Home}/{action=Index}/{id / Se asigna al controlador Home y el


?} método Index ; id se omite

El uso de una plantilla suele ser el método de enrutamiento más sencillo. Las restricciones y los valores
predeterminados también se pueden especificar fuera de la plantilla de ruta.
Sugerencia: Habilite el registro para ver de qué forma las implementaciones de enrutamiento integradas,
como Route , coinciden con las solicitudes.

Referencia de restricción de ruta


Las restricciones de ruta se ejecutan cuando un valor Route ha coincidido con la sintaxis de la dirección
URL entrante y ha convertido la ruta de dirección URL en valores de ruta. En general, las restricciones de
ruta inspeccionan el valor de ruta asociado a través de la plantilla de ruta y deciden si el valor es aceptable o
no. Algunas restricciones de ruta usan datos ajenos al valor de ruta para decidir si la solicitud se puede
enrutar. Por ejemplo, HttpMethodRouteConstraint puede aceptar o rechazar una solicitud basada en su verbo
HTTP.

WARNING
Evite el uso de restricciones para la validación de entradas, porque si lo hace, la entrada no válida producirá un
error "404 No encontrado" en lugar de un error 400 con el mensaje de error adecuado. Las restricciones de ruta
deben usarse para eliminar la ambigüedad entre rutas similares, no para validar las entradas de una ruta
determinada.

En la tabla siguiente se muestran algunas restricciones de ruta y su comportamiento esperado.

RESTRICCIÓN EJEMPLO COINCIDENCIAS DE EJEMPLO NOTAS

int {id:int} 123456789 , -123456789 Coincide con cualquier


entero

bool {active:bool} true , FALSE Coincide con true o


false (no distingue
mayúsculas de minúsculas)

datetime {dob:datetime} 2016-12-31 , Coincide con un valor


2016-12-31 7:32pm DateTime válido (en la
referencia cultural
invariable; vea la
advertencia)

decimal {price:decimal} 49.99 , -1,000.01 Coincide con un valor


decimal válido (en la
referencia cultural
invariable; vea la
advertencia)

double {weight:double} 1.234 , -1,001.01e8 Coincide con un valor


double válido (en la
referencia cultural
invariable; vea la
advertencia)

float {weight:float} 1.234 , -1,001.01e8 Coincide con un valor


float válido (en la
referencia cultural
invariable; vea la
advertencia)

guid {id:guid} CD2C1638-1638-72D5- Coincide con un valor


1638-DEADBEEF1638 Guid válido
,
{CD2C1638-1638-72D5-
1638-DEADBEEF1638}

long {ticks:long} 123456789 , -123456789 Coincide con un valor


long válido

minlength(value) {username:minlength(4)} Rick La cadena debe tener al


menos cuatro caracteres
RESTRICCIÓN EJEMPLO COINCIDENCIAS DE EJEMPLO NOTAS

maxlength(value) {filename:maxlength(8)} Richard La cadena no debe tener


más de ocho caracteres

length(length) {filename:length(12)} somefile.txt La cadena debe tener una


longitud de exactamente
12 caracteres

length(min,max) {filename:length(8,16)} somefile.txt La cadena debe tener una


longitud como mínimo de
ocho caracteres y como
máximo de 16

min(value) {age:min(18)} 19 El valor entero debe ser


como mínimo 18

max(value) {age:max(120)} 91 El valor entero debe ser


como máximo 120

range(min,max) {age:range(18,120)} 91 El valor entero debe ser


como mínimo 18 y
máximo 120

alpha {name:alpha} Rick La cadena debe constar de


uno o más caracteres
alfabéticos ( a - z , no
distingue mayúsculas de
minúsculas)

regex(expression) {ssn:regex(^\\d{{3}}- 123-45-6789 La cadena debe coincidir


\\d{{2}}-\\d{{4}}$)} con la expresión regular
(vea las sugerencias sobre
cómo definir una expresión
regular)

required {name:required} Rick Se usa para exigir que un


valor que no es de
parámetro esté presente
durante la generación de
dirección URL

WARNING
Las restricciones de ruta que comprueban que la dirección URL se puede convertir en un tipo CLR (como int o
DateTime ) usan siempre la referencia cultural invariable, es decir, dan por supuesto que la dirección URL no es
localizable. Las restricciones de ruta proporcionadas por el marco de trabajo no modifican los valores almacenados en
los valores de ruta. Todos los valores de ruta analizados desde la dirección URL se almacenarán como cadenas. Por
ejemplo, la restricción de ruta Float intentará convertir el valor de ruta en un valor Float, pero el valor convertido se
usa exclusivamente para comprobar que se puede convertir en Float.

Expresiones regulares
El marco de trabajo de ASP.NET Core agrega
al constructor de
RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant
expresiones regulares. En RegexOptions Enumeration (Enumeración RegexOptions) puede ver una
descripción de estos miembros.
Las expresiones regulares usan delimitadores y tokens similares a los que usan el enrutamiento y el
lenguaje C#. Es necesario usar secuencias de escape con los tokens de expresiones regulares. Por ejemplo,
para usar la expresión regular ^\d{3}-\d{2}-\d{4}$ en el enrutamiento, es necesario escribir los caracteres
\ como \\ en el archivo de código fuente de C# para que el carácter de escape de cadena \ tenga una
secuencia de escape (a menos que use literales de cadena textuales. Es necesario incluir una secuencia de
escape en los caracteres { , } , "[" y "]". Para ello, duplíquelos a fin de incluir una secuencia de escape en
los caracteres delimitadores del parámetro de enrutamiento. En la tabla siguiente se muestra una expresión
regular y la versión con una secuencia de escape.

EXPRESIÓN NOTA

^\d{3}-\d{2}-\d{4}$ Expresión regular

^\\d{{3}}-\\d{{2}}-\\d{{4}}$ Con secuencia de escape

^[a-z]{2}$ Expresión regular

^[[a-z]]{{2}}$ Con secuencia de escape

Las expresiones regulares usadas en el enrutamiento suelen empezar con el carácter ^ (coincidencia con
la posición inicial de la cadena) y acabar con el carácter $ (coincidencia con la posición final de la cadena).
Los caracteres ^ y $ garantizan que la expresión regular coincide con el valor completo del parámetro de
ruta. Sin los caracteres ^ y $ , la expresión regular coincidirá con cualquier subcadena de la cadena, lo que
a menudo no es deseable. En la tabla siguiente se muestran algunos ejemplos y se explica por qué que
coinciden o no.

EXPRESIÓN STRING COINCIDIR CON COMENTARIO

[a-z]{2} hello sí coincidencias de


subcadenas

[a-z]{2} 123abc456 sí coincidencias de


subcadenas

[a-z]{2} mz sí coincide con la expresión

[a-z]{2} MZ sí no distingue mayúsculas


de minúsculas

^[a-z]{2}$ hello No vea ^ y $ más arriba

^[a-z]{2}$ 123abc456 No vea ^ y $ más arriba

Consulte las expresiones regulares de .NET Framework para obtener más información sobre la sintaxis de
expresiones regulares.
Para restringir un parámetro a un conjunto conocido de valores posibles, use una expresión regular. Por
ejemplo, {action:regex(^(list|get|create)$)} solo hace coincidir el valor de ruta action con list , get o
create . Si se pasa al diccionario de restricciones, la cadena "^(list|get|create)$" sería equivalente. Las
restricciones que se pasan al diccionario de restricciones (no insertado en una plantilla) que no coinciden
con una de las restricciones conocidas también se tratan como expresiones regulares.
Referencia de generación de dirección URL
En el ejemplo siguiente se muestra cómo se genera un vínculo a una ruta, dado un diccionario de valores
de ruta y un valor RouteCollection .

app.Run(async (context) =>


{
var dictionary = new RouteValueDictionary
{
{ "operation", "create" },
{ "id", 123}
};

var vpc = new VirtualPathContext(context, null, dictionary, "Track Package Route");


var path = routes.GetVirtualPath(vpc).VirtualPath;

context.Response.ContentType = "text/html";
await context.Response.WriteAsync("Menu<hr/>");
await context.Response.WriteAsync($"<a href='{path}'>Create Package 123</a><br/>");
});

El valor VirtualPath generado al final del ejemplo anterior es /package/create/123 .


El segundo parámetro del constructor VirtualPathContext es una colección de valores de ambiente. Los
valores de ambiente aportan comodidad al limitar el número de valores que el desarrollador debe
especificar dentro de un determinado contexto de solicitud. Los valores de ruta actuales de la solicitud
actual se consideran valores de ambiente para la generación de vínculos. Por ejemplo, si en una aplicación
ASP.NET MVC se encuentra en la acción About de HomeController , no es necesario especificar el valor de
ruta de controlador para vincular a la acción Index (se usará el valor de ambiente Home ).
Los valores de ambiente se omiten cuando no coinciden con un parámetro y cuando un valor
proporcionado explícitamente lo invalida al ir de izquierda a derecha en la dirección URL.
Los valores que se proporcionan explícitamente pero que no coinciden con nada se agregan a la cadena de
consulta. En la tabla siguiente se muestra el resultado cuando se usa la plantilla de ruta
{controller}/{action}/{id?} .

VALORES DE AMBIENTE VALORES EXPLÍCITOS RESULTADO

controller="Home" action="About" /Home/About

controller="Home" controller="Order",action="About" /Order/About

controller="Home",color="Red" action="About" /Home/About

controller="Home" action="About",color="Red" /Home/About?color=Red

Si una ruta tiene un valor predeterminado que no se corresponde con un parámetro y ese valor se
proporciona explícitamente, debe coincidir con el valor predeterminado. Por ejemplo:

routes.MapRoute("blog_route", "blog/{*slug}",
defaults: new { controller = "Blog", action = "ReadPost" });

La generación de vínculos solo generará un vínculo para esta ruta si se proporcionan los valores
coincidentes para el controlador y la acción.
Middleware de reescritura de URL en ASP.NET Core
17/05/2018 • 31 minutes to read • Edit Online

Por Luke Latham y Mikael Mengistu


Vea o descargue el código de ejemplo (cómo descargarlo)
La reescritura de URL consiste en modificar varias URL de solicitud basadas en una o varias reglas predefinidas.
La reescritura de URL crea una abstracción entre las ubicaciones de recursos y sus direcciones para que las
ubicaciones y direcciones no estén estrechamente vinculadas. Hay varias situaciones en las que la reescritura de
URL es útil:
Mover o reemplazar recursos del servidor de forma temporal o permanente a la vez que se mantienen
estables los localizadores para esos recursos
Dividir el procesamiento de solicitudes entre distintas aplicaciones o entre áreas de una aplicación
Quitar, agregar o reorganizar segmentos de URL en solicitudes entrantes
Optimizar URL públicas para la optimización del motor de búsqueda (SEO )
Permitir el uso de URL públicas preparadas para ayudar a los usuarios a predecir el contenido que
encontrarán siguiendo un vínculo
Redirigir solicitudes no seguras para proteger puntos de conexión
Evitar la creación de vínculos activos de imagen
Puede definir reglas para cambiar la URL de varias maneras, incluidas expresiones regulares, reglas del módulo
mod_rewrite de Apache, reglas del módulo de reescritura de IIS y lógica de regla personalizada. En este
documento se ofrece una introducción a la reescritura de URL e instrucciones sobre cómo usar el middleware de
reescritura de URL en aplicaciones ASP.NET Core.

NOTE
La reescritura de URL puede reducir el rendimiento de una aplicación. Cuando sea factible, debe limitar el número y la
complejidad de las reglas.

Redireccionamiento y reescritura de URL


La diferencia entre los términos redirección de URL y reescritura de URL en principio puede parecer sutil, pero
tiene importantes implicaciones para proporcionar recursos a los clientes. El middleware de reescritura de URL
de ASP.NET Core es capaz de satisfacer las necesidades de ambos.
La redirección de URL es una operación del lado cliente, que da la instrucción al cliente de acceder a un recurso
en otra dirección. Esto requiere un recorrido de ida y vuelta al servidor. La URL de redireccionamiento que se
devuelve al cliente aparece en la barra de direcciones del explorador cuando el cliente realiza una nueva solicitud
para el recurso.
Si /resource se redirige a /different-resource , el cliente solicita /resource . El servidor responde que el cliente
debe obtener el recurso en /different-resource con un código de estado que indica que la redirección es
temporal o permanente. El cliente ejecuta una nueva solicitud para el recurso en la URL de redireccionamiento.
Al redirigir las solicitudes a una URL diferente, se indica si la redirección es permanente o temporal. El código de
estado 301 (Movido definitivamente) se usa cuando el recurso tiene una nueva URL permanente y quiere indicar
al cliente que todas las solicitudes futuras para el recurso deben usar la nueva URL. El cliente puede almacenar
en caché la respuesta cuando se recibe un código de estado 301. El código de estado 302 (Encontrado) se usa
cuando la redirección es temporal o normalmente está sujeta a cambios, de modo que el cliente no debe
almacenar y reutilizar la URL de redireccionamiento en el futuro. Para más información, vea RFC 2616:
definiciones de código de estado.
La reescritura de URL es una operación del lado servidor que proporciona un recurso desde una dirección de
recursos distinta. La reescritura de una URL no requiere un recorrido de ida y vuelta al servidor. La dirección
URL reescrita no se devuelve al cliente y no aparece en la barra de direcciones del explorador. Cuando /resource
se reescribe como /different-resource , el cliente solicita /resource y el servidor recupera internamente el
recurso en /different-resource . Aunque el cliente podría recuperar el recurso en la URL reescrita, el cliente no
informará de que el recurso existe en la URL reescrita cuando realice su solicitud y reciba la respuesta.

Aplicación de ejemplo de reescritura de URL


Puede explorar las características del middleware de reescritura de URL con la aplicación de ejemplo de
reescritura de URL. Esta aplicación aplica reglas de reescritura y redirección, además de mostrar la URL
redirigida o reescrita.

Cuándo usar el middleware de reescritura de URL


Use el middleware de reescritura de URL cuando no pueda usar el módulo de reescritura de URL con IIS en
Windows Server, el módulo mod_rewrite de Apache en el servidor Apache, la reescritura de URL en Nginx o
cuando la aplicación esté hospedada en el servidor HTTP.sys (antes denominado WebListener). Las principales
razones para usar las tecnologías de reescritura de URL basadas en servidor en IIS, Apache o Nginx son que el
middleware no es compatible con todas las características de estos módulos y el rendimiento del middleware
probablemente no coincida con el de los módulos. Pero algunas características de los módulos de servidor no
funcionan con proyectos de ASP.NET Core, como las restricciones IsFile y IsDirectory del módulo de
reescritura de IIS. En estos casos, es mejor usar el middleware.

Package
Para incluir el middleware en el proyecto, agregue una referencia al paquete Microsoft.AspNetCore.Rewrite . Esta
característica está disponible para aplicaciones que tienen como destino ASP.NET Core 1.1 o posterior.
Extensión y opciones
Establezca las reglas de reescritura y redirección de URL mediante la creación de una instancia de la clase
RewriteOptions con métodos de extensión para cada una de las reglas. Encadene varias reglas en el orden que
quiera procesarlas. Las RewriteOptions se pasan al middleware de reescritura de URL cuando se agregan a la
canalización de solicitudes con app.UseRewriter(options); .
ASP.NET Core 2.x
ASP.NET Core 1.x

public void Configure(IApplicationBuilder app)


{
using (StreamReader apacheModRewriteStreamReader =
File.OpenText("ApacheModRewrite.txt"))
using (StreamReader iisUrlRewriteStreamReader =
File.OpenText("IISUrlRewrite.xml"))
{
var options = new RewriteOptions()
.AddRedirect("redirect-rule/(.*)", "redirected/$1")
.AddRewrite(@"^rewrite-rule/(\d+)/(\d+)", "rewritten?var1=$1&var2=$2",
skipRemainingRules: true)
.AddApacheModRewrite(apacheModRewriteStreamReader)
.AddIISUrlRewrite(iisUrlRewriteStreamReader)
.Add(MethodRules.RedirectXMLRequests)
.Add(new RedirectImageRequests(".png", "/png-images"))
.Add(new RedirectImageRequests(".jpg", "/jpg-images"));

app.UseRewriter(options);
}

app.Run(context => context.Response.WriteAsync(


$"Rewritten or Redirected Url: " +
$"{context.Request.Path + context.Request.QueryString}"));
}

Redirección de URL
Use AddRedirect para redirigir las solicitudes. El primer parámetro contiene la expresión regular para hacer
coincidir la ruta de acceso de la URL entrante. El segundo parámetro es la cadena de reemplazo. El tercer
parámetro, si está presente, especifica el código de estado. Si no se especifica el código de estado, el valor
predeterminado es 302 (Encontrado), lo que indica que el recurso se ha movido o reemplazado temporalmente.
ASP.NET Core 2.x
ASP.NET Core 1.x
public void Configure(IApplicationBuilder app)
{
using (StreamReader apacheModRewriteStreamReader =
File.OpenText("ApacheModRewrite.txt"))
using (StreamReader iisUrlRewriteStreamReader =
File.OpenText("IISUrlRewrite.xml"))
{
var options = new RewriteOptions()
.AddRedirect("redirect-rule/(.*)", "redirected/$1")
.AddRewrite(@"^rewrite-rule/(\d+)/(\d+)", "rewritten?var1=$1&var2=$2",
skipRemainingRules: true)
.AddApacheModRewrite(apacheModRewriteStreamReader)
.AddIISUrlRewrite(iisUrlRewriteStreamReader)
.Add(MethodRules.RedirectXMLRequests)
.Add(new RedirectImageRequests(".png", "/png-images"))
.Add(new RedirectImageRequests(".jpg", "/jpg-images"));

app.UseRewriter(options);
}

app.Run(context => context.Response.WriteAsync(


$"Rewritten or Redirected Url: " +
$"{context.Request.Path + context.Request.QueryString}"));
}

En un explorador con herramientas de desarrollo habilitadas, realice una solicitud a la aplicación de ejemplo con
la ruta de acceso /redirect-rule/1234/5678 . La expresión regular coincide con la ruta de acceso de la solicitud en
redirect-rule/(.*) y la ruta de acceso se reemplaza con /redirected/1234/5678 . La URL de redireccionamiento
se devuelve al cliente con un código de estado 302 (Encontrado). El explorador realiza una solicitud nueva en la
URL de redireccionamiento, que aparece en la barra de direcciones del explorador. Puesto que no hay ninguna
regla en la aplicación de ejemplo que coincida con la URL de redireccionamiento, la segunda solicitud recibe una
respuesta 200 (Correcto) de la aplicación y el cuerpo de la respuesta muestra la URL de redireccionamiento. Se
realiza un recorrido de ida y vuelta al servidor cuando se redirige una URL.

WARNING
Tenga cuidado al establecer las reglas de redirección. Las reglas de redirección se evalúan en cada solicitud enviada a la
aplicación, incluso después de una redirección. Es fácil crear por error un bucle de redirecciones infinitas.

Solicitud original: /redirect-rule/1234/5678

La parte de la expresión incluida entre paréntesis se denomina grupo de capturas. El punto ( . ) de la expresión
significa buscar cualquier carácter. El asterisco ( * ) indica buscar el carácter precedente cero o más veces. Por
tanto, el grupo de capturas (.*) busca los dos últimos segmentos de la ruta de acceso de la URL, 1234/5678 .
Cualquier valor que se proporcione en la URL de la solicitud después de redirect-rule/ es capturado por este
grupo de capturas único.
En la cadena de reemplazo, se insertan los grupos capturados en la cadena con el signo de dólar ( $ ) seguido del
número de secuencia de la captura. Se obtiene el valor del primer grupo de capturas con $1 , el segundo con $2
, y así siguen en secuencia para los grupos de capturas de la expresión regular. Solo hay un grupo capturado en la
expresión regular de la regla de redirección de la aplicación de ejemplo, por lo que solo hay un grupo insertado
en la cadena de reemplazo, que es $1 . Cuando se aplica la regla, la URL se convierte en /redirected/1234/5678 .
Redirección de URL a un punto de conexión segura
Use AddRedirectToHttps para redirigir solicitudes HTTP al mismo host y ruta con HTTPS ( https:// ). Si no se
proporciona el código de estado, el middleware muestra de forma predeterminada 302 (Encontrado). Si no se
proporciona el puerto, el middleware muestra de forma predeterminada null , lo que significa que el protocolo
cambia a https:// y el cliente accede al recurso en el puerto 443. En el ejemplo se muestra cómo establecer el
código de estado en 301 (Movido definitivamente) y cambiar el puerto a 5001.

public void Configure(IApplicationBuilder app)


{
var options = new RewriteOptions()
.AddRedirectToHttps(301, 5001);

app.UseRewriter(options);
}

Use AddRedirectToHttpsPermanent para redirigir las solicitudes poco seguras al mismo host y ruta de acceso
mediante el protocolo HTTPS seguro ( https:// en el puerto 443). El middleware establece el código de estado
en 301 (Movido definitivamente).

public void Configure(IApplicationBuilder app)


{
var options = new RewriteOptions()
.AddRedirectToHttpsPermanent();

app.UseRewriter(options);
}

La aplicación de ejemplo es capaz de mostrar cómo usar AddRedirectToHttps o AddRedirectToHttpsPermanent .


Agregue el método de extensión a RewriteOptions . Realice una solicitud poco segura a la aplicación en cualquier
URL. Descarte la advertencia de seguridad del explorador que indica que el certificado autofirmado no es de
confianza.
Solicitud original mediante AddRedirectToHttps(301, 5001) : /secure

Solicitud original mediante AddRedirectToHttpsPermanent : /secure


Reescritura de URL
Use AddRewrite para crear una regla para reescribir URL. El primer parámetro contiene la expresión regular para
buscar coincidencias en la ruta de dirección de URL entrante. El segundo parámetro es la cadena de reemplazo.
El tercer parámetro, skipRemainingRules: {true|false} , indica al middleware si al aplicar la regla actual tiene que
omitir o no alguna regla de redirección adicional.
ASP.NET Core 2.x
ASP.NET Core 1.x

public void Configure(IApplicationBuilder app)


{
using (StreamReader apacheModRewriteStreamReader =
File.OpenText("ApacheModRewrite.txt"))
using (StreamReader iisUrlRewriteStreamReader =
File.OpenText("IISUrlRewrite.xml"))
{
var options = new RewriteOptions()
.AddRedirect("redirect-rule/(.*)", "redirected/$1")
.AddRewrite(@"^rewrite-rule/(\d+)/(\d+)", "rewritten?var1=$1&var2=$2",
skipRemainingRules: true)
.AddApacheModRewrite(apacheModRewriteStreamReader)
.AddIISUrlRewrite(iisUrlRewriteStreamReader)
.Add(MethodRules.RedirectXMLRequests)
.Add(new RedirectImageRequests(".png", "/png-images"))
.Add(new RedirectImageRequests(".jpg", "/jpg-images"));

app.UseRewriter(options);
}

app.Run(context => context.Response.WriteAsync(


$"Rewritten or Redirected Url: " +
$"{context.Request.Path + context.Request.QueryString}"));
}

Solicitud original: /rewrite-rule/1234/5678


Lo primero que se observa en la expresión regular es el acento circunflejo ( ^ ) al principio de la expresión. Esto
significa que la búsqueda de coincidencias empieza al principio de la ruta de dirección de URL.
En el ejemplo anterior con la regla de redirección, redirect-rule/(.*) , no hay ningún acento circunflejo al
principio de la expresión regular; por tanto, cualquier carácter puede preceder a redirect-rule/ en la ruta de
acceso para una coincidencia correcta.

RUTA DE ACCESO COINCIDIR CON

/redirect-rule/1234/5678 Sí

/my-cool-redirect-rule/1234/5678 Sí

/anotherredirect-rule/1234/5678 Sí

La regla de reescritura, ^rewrite-rule/(\d+)/(\d+) , solo encuentra rutas de acceso que empiezan con
rewrite-rule/ . Observe la diferencia de coincidencia entre la siguiente regla de reescritura y la regla de
redirección anterior.

RUTA DE ACCESO COINCIDIR CON

/rewrite-rule/1234/5678 Sí

/my-cool-rewrite-rule/1234/5678 No

/anotherrewrite-rule/1234/5678 No

Después de la parte ^rewrite-rule/ de la expresión, hay dos grupos de captura, (\d+)/(\d+) . \d significa
buscar un dígito (número ). El signo más ( + ) significa buscar uno o más de los caracteres anteriores. Por tanto, la
URL debe contener un número seguido de una barra diagonal, seguida de otro número. Estos grupos de
capturas se insertan en la URL de reescritura como $1 y $2 . La cadena de reemplazo de la regla de reescritura
coloca los grupos capturados en la cadena de consulta. La ruta de acceso solicitada de /rewrite-rule/1234/5678
se reescribe para obtener el recurso en /rewritten?var1=1234&var2=5678 . Si una cadena de consulta está presente
en la solicitud original, se conserva cuando se reescribe la URL.
No hay ningún recorrido de ida y vuelta al servidor para obtener el recurso. Si el recurso existe, se captura y se
devuelve al cliente con un código de estado 200 (Correcto). Como el cliente no se redirige, la URL no cambia en
la barra de direcciones del explorador. En lo que al cliente se refiere, la operación de reescritura de URL nunca se
produjo.
NOTE
Use skipRemainingRules: true siempre que sea posible, ya que las reglas de coincidencia son un proceso costoso y
reducen el tiempo de respuesta de aplicación. Para obtener la respuesta más rápida de la aplicación:
Ordene las reglas de reescritura desde la que coincida con más frecuencia a la que coincida con menos frecuencia.
Omita el procesamiento de las reglas restantes cuando se produzca una coincidencia; no es necesario ningún
procesamiento de reglas adicional.

mod_rewrite de Apache
Aplique reglas mod_rewrite de Apache con AddApacheModRewrite . Asegúrese de que el archivo de reglas se
implementa con la aplicación. Para obtener más información y ejemplos de reglas mod_rewrite, vea Apache
mod_rewrite (mod_rewrite de Apache).
ASP.NET Core 2.x
ASP.NET Core 1.x
Se usa StreamReader para leer las reglas del archivo de reglas ApacheModRewrite.txt.

public void Configure(IApplicationBuilder app)


{
using (StreamReader apacheModRewriteStreamReader =
File.OpenText("ApacheModRewrite.txt"))
using (StreamReader iisUrlRewriteStreamReader =
File.OpenText("IISUrlRewrite.xml"))
{
var options = new RewriteOptions()
.AddRedirect("redirect-rule/(.*)", "redirected/$1")
.AddRewrite(@"^rewrite-rule/(\d+)/(\d+)", "rewritten?var1=$1&var2=$2",
skipRemainingRules: true)
.AddApacheModRewrite(apacheModRewriteStreamReader)
.AddIISUrlRewrite(iisUrlRewriteStreamReader)
.Add(MethodRules.RedirectXMLRequests)
.Add(new RedirectImageRequests(".png", "/png-images"))
.Add(new RedirectImageRequests(".jpg", "/jpg-images"));

app.UseRewriter(options);
}

app.Run(context => context.Response.WriteAsync(


$"Rewritten or Redirected Url: " +
$"{context.Request.Path + context.Request.QueryString}"));
}

La aplicación de ejemplo redirige las solicitudes de /apache-mod-rules-redirect/(.\*) a /redirected?id=$1 . El


código de estado de la respuesta es 302 (Encontrado).

# Rewrite path with additional sub directory


RewriteRule ^/apache-mod-rules-redirect/(.*) /redirected?id=$1 [L,R=302]

Solicitud original: /apache-mod-rules-redirect/1234


Va r i a b l e s d e se r v i d o r c o m p a t i b l e s

El middleware admite las siguientes variables de servidor mod_rewrite de Apache:


CONN_REMOTE_ADDR
HTTP_ACCEPT
HTTP_CONNECTION
HTTP_COOKIE
HTTP_FORWARDED
HTTP_HOST
HTTP_REFERER
HTTP_USER_AGENT
HTTPS
IPV6
QUERY_STRING
REMOTE_ADDR
REMOTE_PORT
REQUEST_FILENAME
REQUEST_METHOD
REQUEST_SCHEME
REQUEST_URI
SCRIPT_FILENAME
SERVER_ADDR
SERVER_PORT
SERVER_PROTOCOL
TIME
TIME_DAY
TIME_HOUR
TIME_MIN
TIME_MON
TIME_SEC
TIME_WDAY
TIME_YEAR
Reglas del Módulo URL Rewrite para IIS
Para usar reglas que se apliquen al Módulo URL Rewrite para IIS, use AddIISUrlRewrite . Asegúrese de que el
archivo de reglas se implementa con la aplicación. No dirija el middleware para que use el archivo web.config
cuando se ejecute en Windows Server IIS. Con IIS, estas reglas deben almacenarse fuera de web.config para
evitar conflictos con el Módulo URL Rewrite para IIS. Para obtener más información y ejemplos de reglas del
Módulo URL Rewrite para IIS, vea Using Url Rewrite Module 2.0 (Uso del Módulo URL Rewrite 2.0) y URL
Rewrite Module Configuration Reference (Referencia de configuración del Módulo URL Rewrite).
ASP.NET Core 2.x
ASP.NET Core 1.x
Se usa StreamReader para leer las reglas del archivo de reglas IISUrlRewrite.xml.

public void Configure(IApplicationBuilder app)


{
using (StreamReader apacheModRewriteStreamReader =
File.OpenText("ApacheModRewrite.txt"))
using (StreamReader iisUrlRewriteStreamReader =
File.OpenText("IISUrlRewrite.xml"))
{
var options = new RewriteOptions()
.AddRedirect("redirect-rule/(.*)", "redirected/$1")
.AddRewrite(@"^rewrite-rule/(\d+)/(\d+)", "rewritten?var1=$1&var2=$2",
skipRemainingRules: true)
.AddApacheModRewrite(apacheModRewriteStreamReader)
.AddIISUrlRewrite(iisUrlRewriteStreamReader)
.Add(MethodRules.RedirectXMLRequests)
.Add(new RedirectImageRequests(".png", "/png-images"))
.Add(new RedirectImageRequests(".jpg", "/jpg-images"));

app.UseRewriter(options);
}

app.Run(context => context.Response.WriteAsync(


$"Rewritten or Redirected Url: " +
$"{context.Request.Path + context.Request.QueryString}"));
}

La aplicación de ejemplo reescribe las solicitudes de /iis-rules-rewrite/(.*) a /rewritten?id=$1 . La respuesta


se envía al cliente con un código de estado 200 (Correcto).

<rewrite>
<rules>
<rule name="Rewrite segment to id querystring" stopProcessing="true">
<match url="^iis-rules-rewrite/(.*)$" />
<action type="Rewrite" url="rewritten?id={R:1}" appendQueryString="false"/>
</rule>
</rules>
</rewrite>

Solicitud original: /iis-rules-rewrite/1234


Si tiene un Módulo URL Rewrite para IIS activo con reglas configuradas en el nivel de servidor que podrían
afectar a la aplicación de manera no deseada, puede deshabilitar el Módulo URL Rewrite para IIS para una
aplicación. Para más información, vea Disabling IIS modules (Deshabilitación de módulos de IIS ).
Características no admitidas
ASP.NET Core 2.x
ASP.NET Core 1.x
El middleware publicado con ASP.NET Core 2.x no admite las siguientes características de Módulo URL Rewrite
para IIS:
Reglas de salida
Variables de servidor personalizadas
Caracteres comodín
LogRewrittenUrl
Variables de servidor compatibles
El middleware admite las siguientes variables de servidor del Módulo URL Rewrite para IIS:
CONTENT_LENGTH
CONTENT_TYPE
HTTP_ACCEPT
HTTP_CONNECTION
HTTP_COOKIE
HTTP_HOST
HTTP_REFERER
HTTP_URL
HTTP_USER_AGENT
HTTPS
LOCAL_ADDR
QUERY_STRING
REMOTE_ADDR
REMOTE_PORT
REQUEST_FILENAME
REQUEST_URI

NOTE
También puede obtener IFileProvider a través de PhysicalFileProvider . Con este enfoque logrará mayor flexibilidad
para la ubicación de los archivos de reglas de reescritura. Asegúrese de que los archivos de reglas de reescritura se
implementan en el servidor en la ruta de acceso que proporcione.

PhysicalFileProvider fileProvider = new PhysicalFileProvider(Directory.GetCurrentDirectory());

Regla basada en métodos


Use Add(Action<RewriteContext> applyRule) para implementar su propia lógica de la regla en un método.
RewriteContext expone el HttpContext para usarlo en el método. context.Result determina cómo se administra
el procesamiento adicional en la canalización.
CONTEX T.RESULT ACCIÓN

RuleResult.ContinueRules (valor predeterminado) Continuar aplicando reglas

RuleResult.EndResponse Dejar de aplicar reglas y enviar la respuesta

RuleResult.SkipRemainingRules Dejar de aplicar reglas y enviar el contexto al siguiente


middleware

ASP.NET Core 2.x


ASP.NET Core 1.x

public void Configure(IApplicationBuilder app)


{
using (StreamReader apacheModRewriteStreamReader =
File.OpenText("ApacheModRewrite.txt"))
using (StreamReader iisUrlRewriteStreamReader =
File.OpenText("IISUrlRewrite.xml"))
{
var options = new RewriteOptions()
.AddRedirect("redirect-rule/(.*)", "redirected/$1")
.AddRewrite(@"^rewrite-rule/(\d+)/(\d+)", "rewritten?var1=$1&var2=$2",
skipRemainingRules: true)
.AddApacheModRewrite(apacheModRewriteStreamReader)
.AddIISUrlRewrite(iisUrlRewriteStreamReader)
.Add(MethodRules.RedirectXMLRequests)
.Add(new RedirectImageRequests(".png", "/png-images"))
.Add(new RedirectImageRequests(".jpg", "/jpg-images"));

app.UseRewriter(options);
}

app.Run(context => context.Response.WriteAsync(


$"Rewritten or Redirected Url: " +
$"{context.Request.Path + context.Request.QueryString}"));
}

La aplicación de ejemplo muestra un método que redirige las solicitudes para las rutas de acceso que terminen
con .xml. Si realiza una solicitud para /file.xml , se redirige a /xmlfiles/file.xml . El código de estado se
establece en 301 (Movido definitivamente). Para una redirección, debe establecer explícitamente el código de
estado de la respuesta; en caso contrario, se devuelve un código de estado 200 (Correcto) y no se produce la
redirección en el cliente.
public static void RedirectXMLRequests(RewriteContext context)
{
var request = context.HttpContext.Request;

// Because we're redirecting back to the same app, stop


// processing if the request has already been redirected
if (request.Path.StartsWithSegments(new PathString("/xmlfiles")))
{
return;
}

if (request.Path.Value.EndsWith(".xml", StringComparison.OrdinalIgnoreCase))
{
var response = context.HttpContext.Response;
response.StatusCode = StatusCodes.Status301MovedPermanently;
context.Result = RuleResult.EndResponse;
response.Headers[HeaderNames.Location] =
"/xmlfiles" + request.Path + request.QueryString;
}
}

Solicitud original: /file.xml

Regla basada en IRule


Use Add(IRule) para implementar su propia lógica de la regla en una clase que deriva de IRule . Al usar IRule ,
se logra mayor flexibilidad que si se usa el enfoque de reglas basadas en métodos. La clase derivada puede
incluir un constructor, donde puede pasar parámetros para el método ApplyRule .
ASP.NET Core 2.x
ASP.NET Core 1.x
public void Configure(IApplicationBuilder app)
{
using (StreamReader apacheModRewriteStreamReader =
File.OpenText("ApacheModRewrite.txt"))
using (StreamReader iisUrlRewriteStreamReader =
File.OpenText("IISUrlRewrite.xml"))
{
var options = new RewriteOptions()
.AddRedirect("redirect-rule/(.*)", "redirected/$1")
.AddRewrite(@"^rewrite-rule/(\d+)/(\d+)", "rewritten?var1=$1&var2=$2",
skipRemainingRules: true)
.AddApacheModRewrite(apacheModRewriteStreamReader)
.AddIISUrlRewrite(iisUrlRewriteStreamReader)
.Add(MethodRules.RedirectXMLRequests)
.Add(new RedirectImageRequests(".png", "/png-images"))
.Add(new RedirectImageRequests(".jpg", "/jpg-images"));

app.UseRewriter(options);
}

app.Run(context => context.Response.WriteAsync(


$"Rewritten or Redirected Url: " +
$"{context.Request.Path + context.Request.QueryString}"));
}

Se comprueba que los valores de los parámetros en la aplicación de ejemplo para extension y newPath
cumplen ciertas condiciones. extension debe contener un valor, que debe ser .png, .jpg o .gif. Si newPath no es
válido, se genera ArgumentException . Si se realiza una solicitud para image.png, se redirige a
/png-images/image.png . Si se realiza una solicitud para image.jpg, se redirige a /jpg-images/image.jpg . El código
de estado se establece en 301 (Movido definitivamente) y se establece context.Result para detener el
procesamiento de reglas y enviar la respuesta.
public class RedirectImageRequests : IRule
{
private readonly string _extension;
private readonly PathString _newPath;

public RedirectImageRequests(string extension, string newPath)


{
if (string.IsNullOrEmpty(extension))
{
throw new ArgumentException(nameof(extension));
}

if (!Regex.IsMatch(extension, @"^\.(png|jpg|gif)$"))
{
throw new ArgumentException("Invalid extension", nameof(extension));
}

if (!Regex.IsMatch(newPath, @"(/[A-Za-z0-9]+)+?"))
{
throw new ArgumentException("Invalid path", nameof(newPath));
}

_extension = extension;
_newPath = new PathString(newPath);
}

public void ApplyRule(RewriteContext context)


{
var request = context.HttpContext.Request;

// Because we're redirecting back to the same app, stop


// processing if the request has already been redirected
if (request.Path.StartsWithSegments(new PathString(_newPath)))
{
return;
}

if (request.Path.Value.EndsWith(_extension, StringComparison.OrdinalIgnoreCase))
{
var response = context.HttpContext.Response;
response.StatusCode = StatusCodes.Status301MovedPermanently;
context.Result = RuleResult.EndResponse;
response.Headers[HeaderNames.Location] =
_newPath + request.Path + request.QueryString;
}
}
}

Solicitud original: /image.png

Solicitud original: /image.jpg


Ejemplos de expresiones regulares
CADENA DE EXPRESIÓN REGULAR & CADENA DE REEMPLAZO &
OBJETIVO EJEMPLO DE COINCIDENCIA EJEMPLO DE RESULTADO

Ruta de acceso de reescritura en la ^path/(.*)/(.*) path?var1=$1&var2=$2


cadena de consulta /path/abc/123 /path?var1=abc&var2=123

Quitar barra diagonal final (.*)/$ $1


/path/ /path

Exigir barra diagonal final (.*[^/])$ $1/


/path /path/

Evitar reescritura de solicitudes ^(.*)(?<!\.axd)$ o rewritten/$1


específicas ^(?!.*\.axd$)(.*)$ /rewritten/resource.htm
Sí: /resource.htm /resource.axd
No: /resource.axd

Reorganizar los segmentos de URL path/(.*)/(.*)/(.*) path/$3/$2/$1


path/1/2/3 path/3/2/1

Reemplazar un segmento de URL ^(.*)/segment2/(.*) $1/replaced/$2


/segment1/segment2/segment3 /segment1/replaced/segment3

Recursos adicionales
Inicio de aplicaciones
Middleware
Expresiones regulares en .NET
Lenguaje de expresiones regulares: referencia rápida
Apache mod_rewrite (mod_rewrite de Apache)
Using Url Rewrite Module 2.0 (for IIS ) (Uso del Módulo URL Rewrite 2.0 para IIS )
URL Rewrite Module Configuration Reference (Referencia de configuración del Módulo URL Rewrite)
IIS URL Rewrite Module Forum (Foro del Módulo URL Rewrite para IIS )
Cómo simplificar la estructura de direcciones URL
10 URL Rewriting Tips and Tricks (10 trucos y consejos para reescritura de URL )
To slash or not to slash (Usar la barra diagonal o no)
Usar varios entornos en ASP.NET Core
19/06/2018 • 9 minutes to read • Edit Online

Por Rick Anderson


ASP.NET Core proporciona compatibilidad para establecer el comportamiento de las aplicaciones en tiempo
de ejecución con variables de entorno.
Vea o descargue el código de ejemplo (cómo descargarlo)

Entornos
ASP.NET Core lee la variable de entorno ASPNETCORE_ENVIRONMENT al inicio de la aplicación y almacena ese
valor en IHostingEnvironment.EnvironmentName. ASPNETCORE_ENVIRONMENT se puede establecer en cualquier
valor, pero el marco de trabajo admite tres valores: Development, Staging y Production. Si no se ha
establecido ASPNETCORE_ENVIRONMENT , el valor predeterminado será Production .

public void Configure(IApplicationBuilder app, IHostingEnvironment env)


{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}

if (env.IsProduction() || env.IsStaging() || env.IsEnvironment("Staging_2"))


{
app.UseExceptionHandler("/Error");
}

app.UseStaticFiles();
app.UseMvcWithDefaultRoute();
}

El código anterior:
Llama a UseDeveloperExceptionPage y UseBrowserLink cuando ASPNETCORE_ENVIRONMENT está
establecido en Development .
Llama a UseExceptionHandler cuando el valor de ASPNETCORE_ENVIRONMENT está establecido en uno de
los siguientes:
Staging
Production
Staging_2

La aplicación auxiliar de etiquetas de entorno usa el valor de IHostingEnvironment.EnvironmentName para incluir


o excluir el marcado en el elemento:
@page
@inject Microsoft.AspNetCore.Hosting.IHostingEnvironment hostingEnv
@model AboutModel
@{
ViewData["Title"] = "About";
}
<h2>@ViewData["Title"]</h2>
<h3>@Model.Message</h3>

<p> ASPNETCORE_ENVIRONMENT = @hostingEnv.EnvironmentName</p>

<environment include="Development">
<div>&lt;environment include="Development"&gt;</div>
</environment>
<environment exclude="Development">
<div>&lt;environment exclude="Development"&gt;</div>
</environment>
<environment include="Staging,Development,Staging_2">
<div>
&lt;environment include="Staging,Development,Staging_2"&gt;
</div>
</environment>

Nota: En Windows y macOS, los valores y las variables de entorno no distinguen mayúsculas de minúsculas.
Los valores y las variables de entorno de Linux distinguen mayúsculas de minúsculas de forma
predeterminada.
Desarrollo
El entorno de desarrollo puede habilitar características que no deben exponerse en producción. Por ejemplo,
las plantillas de ASP.NET Core habilitan la página de excepciones para el desarrollador en el entorno de
desarrollo.
El entorno para el desarrollo del equipo local se puede establecer en el archivo Properties\launchSettings.json
del proyecto. Los valores de entorno establecidos en launchSettings.json invalidan los valores establecidos en
el entorno del sistema.
En el siguiente fragmento de JSON se muestran tres perfiles de un archivo launchSettings.json:
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:54339/",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"WebApp1": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Staging"
},
"applicationUrl": "http://localhost:54340/"
},
"Kestrel Staging": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_My_Environment": "1",
"ASPNETCORE_DETAILEDERRORS": "1",
"ASPNETCORE_ENVIRONMENT": "Staging"
},
"applicationUrl": "http://localhost:51997/"
}
}
}

NOTE
La propiedad applicationUrl en launchSettings.json puede especificar una lista de direcciones URL del servidor. Use
un punto y coma entre las direcciones URL de la lista:

"WebApplication1": {
"commandName": "Project",
"launchBrowser": true,
"applicationUrl": "https://localhost:5001;http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}

Cuando la aplicación se inicia con dotnet run, se usará el primer perfil con "commandName": "Project" . El valor
de commandName especifica el servidor web que se va a iniciar. commandName puede ser una de las siguientes
opciones:
IIS Express
IIS
Project (que inicia Kestrel)
Cuando una aplicación se inicia con dotnet run:
Se lee launchSettings.json, si está disponible. La configuración de environmentVariables de
launchSettings.json reemplaza las variables de entorno.
Se muestra el entorno de hospedaje.
En la siguiente salida se muestra una aplicación que se ha iniciado con dotnet run:

PS C:\Webs\WebApp1> dotnet run


Using launch settings from C:\Webs\WebApp1\Properties\launchSettings.json...
Hosting environment: Staging
Content root path: C:\Webs\WebApp1
Now listening on: http://localhost:54340
Application started. Press Ctrl+C to shut down.

La pestaña Depurar de Visual Studio proporciona una GUI para editar el archivo launchSettings.json:

Los cambios realizados en los perfiles de proyecto podrían no surtir efecto hasta que se reinicie el servidor
web. Es necesario reiniciar Kestrel para que detecte los cambios realizados en su entorno.

WARNING
En launchSettings.json no se deben almacenar secretos. Se puede usar la herramienta Administrador de secretos a fin
de almacenar secretos para el desarrollo local.

Producción
El entorno de producción debe configurarse para maximizar la seguridad, el rendimiento y la solidez de la
aplicación. Las opciones de configuración comunes que difieren de las del entorno de desarrollo son:
Almacenamiento en caché.
Los recursos del cliente se agrupan, se reducen y se atienden potencialmente desde una CDN.
Las páginas de error de diagnóstico están deshabilitadas.
Las páginas de error descriptivas están habilitadas.
El registro y la supervisión de la producción están habilitados. Por ejemplo, Application Insights.

Establecimiento del entorno


A menudo resulta útil establecer un entorno específico para la realización de pruebas. Si el entorno no está
establecido, el valor predeterminado será Production , lo que deshabilita la mayoría de las características de
depuración.
El método para establecer el entorno depende del sistema operativo.
Azure
Para Azure App Service:
Seleccione la hoja Configuración de la aplicación.
Agregue la clave y el valor en Configuración de la aplicación.
Windows
Para establecer ASPNETCORE_ENVIRONMENT en la sesión actual, si la aplicación se ha iniciado con dotnet run, use
los comandos siguientes:
Línea de comandos

set ASPNETCORE_ENVIRONMENT=Development

PowerShell

$Env:ASPNETCORE_ENVIRONMENT = "Development"

Estos comandos solo tienen efecto en la ventana actual. Cuando se cierre la ventana, la configuración de
ASPNETCORE_ENVIRONMENT volverá a la configuración predeterminada o al valor del equipo. Para
establecer el valor globalmente en Windows, abra Panel de Control > Sistema > Configuración
avanzada del sistema y agregue o edite el valor ASPNETCORE_ENVIRONMENT .
web.config
Vea la sección Establecer variables de entorno del tema Referencia de configuración del módulo ASP.NET
Core.
Por grupo de aplicaciones de IIS
Para establecer variables de entorno para aplicaciones individuales que se ejecutan en grupos de aplicaciones
aislados (se admite en IIS 10.0+), vea la sección AppCmd.exe command (Comando AppCmd.exe) del tema
Environment Variables <environmentVariables> (Variables de entorno ).
macOS
Para establecer el entorno actual para macOS, puede hacerlo en línea al ejecutar la aplicación.

ASPNETCORE_ENVIRONMENT=Development dotnet run

También puede usar export para establecerlo antes de ejecutar la aplicación.

export ASPNETCORE_ENVIRONMENT=Development

Las variables de entorno de nivel de equipo se establecen en el archivo .bashrc o .bash_profile. Para editar el
archivo, use cualquier editor de texto y agregue la instrucción siguiente.

export ASPNETCORE_ENVIRONMENT=Development

Linux
Para distribuciones de Linux, use el comando export en la línea de comandos para la configuración de
variables basada en sesión y el archivo bash_profile para la configuración del entorno en el nivel de equipo.
Configuración de entorno
Para más información, vea Configuración de entorno.

Métodos y clase Startup basados en entorno


Cuando se inicia una aplicación ASP.NET Core, la clase Startup arranca la aplicación. Si existe una clase
Startup{EnvironmentName} , se llamará para ese EnvironmentName :
public class StartupDevelopment
{
public StartupDevelopment(IConfiguration configuration)
{
Configuration = configuration;
}

public IConfiguration Configuration { get; }

public void ConfigureServices(IServiceCollection services)


{
services.AddMvc();
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)


{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}

if (env.IsProduction() || env.IsStaging())
{
throw new Exception("Not development.");
}

app.UseStaticFiles();
app.UseMvcWithDefaultRoute();
}
}

Nota: Al llamar a WebHostBuilder.UseStartup se invalidan las secciones de configuración.


Configure y ConfigureServices son compatibles con versiones específicas del entorno con el formato
Configure{EnvironmentName} y Configure{EnvironmentName}Services :
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}

public IConfiguration Configuration { get; }

public void ConfigureServices(IServiceCollection services)


{
services.AddMvc();
}

public void ConfigureStagingServices(IServiceCollection services)


{
services.AddMvc();
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)


{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}

if (env.IsProduction() || env.IsStaging() || env.IsEnvironment("Staging_2"))


{
app.UseExceptionHandler("/Error");
}

app.UseStaticFiles();
app.UseMvcWithDefaultRoute();
}

public void ConfigureStaging(IApplicationBuilder app, IHostingEnvironment env)


{
if (!env.IsStaging())
{
throw new Exception("Not staging.");
}

app.UseExceptionHandler("/Error");
app.UseStaticFiles();
app.UseMvcWithDefaultRoute();
}
}

Recursos adicionales
Inicio de aplicaciones
Configuración
IHostingEnvironment.EnvironmentName
Configuración en ASP.NET Core
21/06/2018 • 27 minutes to read • Edit Online

Por Rick Anderson, Mark Michaelis, Steve Smith, Daniel Roth y Luke Latham
La API de configuración proporciona una manera de configurar una aplicación web ASP.NET Core según una lista
de pares de nombre y valor. La configuración se lee en tiempo de ejecución desde varios orígenes. Puede agrupar
estos pares nombre-valor en una jerarquía multinivel.
Existen proveedores de configuración para:
Formatos de archivo (INI, JSON y XML ).
Argumentos de la línea de comandos.
Variables de entorno.
Objetos de .NET en memoria.
El almacenamiento de administrador secreto sin cifrar.
Un almacén de usuario cifrado, como Azure Key Vault.
Proveedores personalizados (instalados o creados).
Cada valor de configuración se asigna a una clave de cadena. Hay compatibilidad de enlace integrada para
deserializar la configuración en un objeto POCO personalizado (una clase simple de .NET con propiedades).
El patrón de opciones usa las clases de opciones para representar grupos de configuraciones relacionadas. Para
más información sobre cómo usar el patrón de opciones, vea el tema Opciones.
Vea o descargue el código de ejemplo (cómo descargarlo)

Configuración de JSON
En la aplicación de consola siguiente se usa el proveedor de configuración de JSON:
using System;
using System.IO;
// Requires NuGet package
// Microsoft.Extensions.Configuration.Json
using Microsoft.Extensions.Configuration;

public class Program


{
public static IConfiguration Configuration { get; set; }

public static void Main(string[] args = null)


{
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json");

Configuration = builder.Build();

Console.WriteLine($"option1 = {Configuration["Option1"]}");
Console.WriteLine($"option2 = {Configuration["option2"]}");
Console.WriteLine(
$"suboption1 = {Configuration["subsection:suboption1"]}");
Console.WriteLine();

Console.WriteLine("Wizards:");
Console.Write($"{Configuration["wizards:0:Name"]}, ");
Console.WriteLine($"age {Configuration["wizards:0:Age"]}");
Console.Write($"{Configuration["wizards:1:Name"]}, ");
Console.WriteLine($"age {Configuration["wizards:1:Age"]}");
Console.WriteLine();

Console.WriteLine("Press a key...");
Console.ReadKey();
}
}

La aplicación lee y muestra los valores de configuración siguientes:

{
"option1": "value1_from_json",
"option2": 2,

"subsection": {
"suboption1": "subvalue1_from_json"
},
"wizards": [
{
"Name": "Gandalf",
"Age": "1000"
},
{
"Name": "Harry",
"Age": "17"
}
]
}

La configuración consta de una lista jerárquica de pares de nombre y valor en los que los nodos se separan con dos
puntos ( : ). Para recuperar un valor, obtenga acceso al indizador Configuration con la clave del elemento
correspondiente:
Console.WriteLine(
$"suboption1 = {Configuration["subsection:suboption1"]}");

Para trabajar con matrices en orígenes de configuración con formato JSON, use un índice de matriz como parte de
la cadena separada por dos puntos. En el ejemplo siguiente se obtiene el nombre del primer elemento de la matriz
wizards anterior:

Console.Write($"{Configuration["wizards:0:Name"]}");
// Output: Gandalf

Los pares de nombre y valor que se escriben en los proveedores de Configuración integrados no se conservan,
pero se puede crear un proveedor personalizado que guarde los valores. Vea Proveedor de configuración
personalizado.
En el ejemplo anterior se usa el indizador de configuración para leer los valores. Para obtener acceso a la
configuración fuera de Startup , use el patrón de opciones. Para más información, vea el tema Opciones.

Configuración XML
Para trabajar con matrices en orígenes de configuración con formato XML, proporcione un índice name a cada
elemento. Use el índice para acceder a los valores:

<wizards>
<wizard name="Gandalf">
<age>1000</age>
</wizard>
<wizard name="Harry">
<age>17</age>
</wizard>
</wizards>

Console.Write($"{Configuration["wizard:Harry:age"]}");
// Output: 17

Configuración de entorno
Es habitual tener distintos valores de configuración para entornos diferentes, por ejemplo para desarrollo, pruebas
y producción. El método de extensión CreateDefaultBuilder en una aplicación de ASP.NET Core 2.x (o mediante
AddJsonFile y AddEnvironmentVariables directamente en una aplicación de ASP.NET Core 1.x) agrega proveedores
de configuración para leer archivos JSON y orígenes de configuración del sistema:
appsettings.json
appsettings.<NombreDelEntorno>.json
Variables de entorno
Las aplicaciones ASP.NET Core 1.x deben llamar a AddJsonFile y AddEnvironmentVariables.
Vea AddJsonFile para obtener una explicación de los parámetros. reloadOnChange solo se admite en ASP.NET Core
1.1 y versiones posteriores.
Los orígenes de configuración se leen en el orden en que se especifican. En el código anterior, las variables de
entorno se leen en último lugar. Cualquier valor de configuración establecido mediante el entorno reemplaza a los
establecidos en los dos proveedores anteriores.
Tenga en cuenta el siguiente archivo appsettings.Staging.json:

{
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"System": "Information",
"Microsoft": "Information"
}
},
"MyConfig": "My Config Value for staging."
}

En el siguiente código, Configure lee el valor de MyConfig :

public void Configure(IApplicationBuilder app, IHostingEnvironment env)


{
var myConfig = Configuration["MyConfig"];
// use myConfig
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}

if (env.IsProduction() || env.IsStaging())
{
app.UseExceptionHandler("/Error");
}

app.UseStaticFiles();
app.UseMvcWithDefaultRoute();
}

El entorno se establece normalmente en Development , Staging o Production . Para obtener más información,
consulte Uso de varios entornos.
Consideraciones de configuración:
IOptionsSnapshot puede volver a cargar los datos de configuración cuando cambia.
En las claves de configuraciones no se distingue entre mayúsculas y minúsculas.
Nunca almacene contraseñas u otros datos confidenciales en el código del proveedor de configuración o en
archivos de configuración de texto sin formato. No use secretos de producción en los entornos de desarrollo o
pruebas. Especifique los secretos fuera del proyecto para que no se confirmen en un repositorio de código
fuente de manera accidental. Obtenga más información sobre cómo usar con varios entornos y administrar el
almacenamiento seguro de los secretos de aplicación en el desarrollo.
Para los valores de configuración jerárquica especificados en las variables de entorno, el signo de dos puntos (
: ) podría no funcionar en todas las plataformas. El guion bajo doble ( __ ) es compatible con todas las
plataformas.
Al interactuar con la API de configuración, el signo de dos puntos ( : ) funciona en todas las plataformas.

Proveedor en memoria y enlace a una clase POCO


En el ejemplo siguiente se muestra cómo usar el proveedor en memoria y enlazar a una clase:
using System;
using System.Collections.Generic;
using Microsoft.Extensions.Configuration;

public class Program


{
public static IConfiguration Configuration { get; set; }

public static void Main(string[] args = null)


{
var dict = new Dictionary<string, string>
{
{"Profile:MachineName", "Rick"},
{"App:MainWindow:Height", "11"},
{"App:MainWindow:Width", "11"},
{"App:MainWindow:Top", "11"},
{"App:MainWindow:Left", "11"}
};

var builder = new ConfigurationBuilder();


builder.AddInMemoryCollection(dict);

Configuration = builder.Build();

Console.WriteLine($"Hello {Configuration["Profile:MachineName"]}");

var window = new MyWindow();


// Bind requrires NuGet package
// Microsoft.Extensions.Configuration.Binder
Configuration.GetSection("App:MainWindow").Bind(window);
Console.WriteLine($"Left {window.Left}");
Console.WriteLine();

Console.WriteLine("Press any key...");


Console.ReadKey();
}
}

public class MyWindow


{
public int Height { get; set; }
public int Width { get; set; }
public int Top { get; set; }
public int Left { get; set; }
}

Los valores de configuración se devuelven como cadenas, pero el enlace permite la construcción de objetos. El
enlace permite recuperar objetos POCO o incluso gráficos de objetos completos.
GetValue
En el ejemplo siguiente se muestra el método de extensión GetValue<T>:
using System;
using System.Collections.Generic;
using Microsoft.Extensions.Configuration;

public class Program


{
public static IConfiguration Configuration { get; set; }

public static void Main(string[] args = null)


{
var dict = new Dictionary<string, string>
{
{"Profile:MachineName", "Rick"},
{"App:MainWindow:Height", "11"},
{"App:MainWindow:Width", "11"},
{"App:MainWindow:Top", "11"},
{"App:MainWindow:Left", "11"}
};

var builder = new ConfigurationBuilder();


builder.AddInMemoryCollection(dict);

Configuration = builder.Build();

Console.WriteLine($"Hello {Configuration["Profile:MachineName"]}");

// Show GetValue overload and set the default value to 80


// Requires NuGet package "Microsoft.Extensions.Configuration.Binder"
var left = Configuration.GetValue<int>("App:MainWindow:Left", 80);
Console.WriteLine($"Left {left}");

var window = new MyWindow();


Configuration.GetSection("App:MainWindow").Bind(window);
Console.WriteLine($"Left {window.Left}");
Console.WriteLine();

Console.WriteLine("Press a key...");
Console.ReadKey();
}
}

public class MyWindow


{
public int Height { get; set; }
public int Width { get; set; }
public int Top { get; set; }
public int Left { get; set; }
}

El método GetValue<T> de ConfigurationBinder permite especificar un valor predeterminado (80 en el ejemplo).


GetValue<T> es para escenarios sencillos y no se enlaza con secciones completas. GetValue<T> obtiene los valores
escalares de GetSection(key).Value convertidos a un tipo específico.

Enlazar a un gráfico de objetos


Cada objeto de una clase se puede enlazar de forma recursiva. Observe la clase AppSettings siguiente:
public class AppSettings
{
public Window Window { get; set; }
public Connection Connection { get; set; }
public Profile Profile { get; set; }
}

public class Window


{
public int Height { get; set; }
public int Width { get; set; }
}

public class Connection


{
public string Value { get; set; }
}

public class Profile


{
public string Machine { get; set; }
}

El ejemplo siguiente se enlaza a la clase AppSettings :

using System;
using System.IO;
using Microsoft.Extensions.Configuration;

public class Program


{
public static void Main(string[] args = null)
{
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json");

var config = builder.Build();

var appConfig = new AppSettings();


config.GetSection("App").Bind(appConfig);

Console.WriteLine($"Height {appConfig.Window.Height}");
Console.WriteLine();

Console.WriteLine("Press a key...");
Console.ReadKey();
}
}

En ASP.NET Core 1.1 y versiones posteriores se puede usar Get<T> , que funciona con secciones completas.
Get<T> puede ser más conveniente que usar Bind . En el código siguiente se muestra cómo usar Get<T> con el
ejemplo anterior:

var appConfig = config.GetSection("App").Get<AppSettings>();

Con el siguiente archivo appsettings.json:


{
"App": {
"Profile": {
"Machine": "Rick"
},
"Connection": {
"Value": "connectionstring"
},
"Window": {
"Height": "11",
"Width": "11"
}
}
}

El programa muestra Height 11 .


El código siguiente se puede usar para pruebas unitarias de la configuración:

[Fact]
public void CanBindObjectTree()
{
var dict = new Dictionary<string, string>
{
{"App:Profile:Machine", "Rick"},
{"App:Connection:Value", "connectionstring"},
{"App:Window:Height", "11"},
{"App:Window:Width", "11"}
};
var builder = new ConfigurationBuilder();
builder.AddInMemoryCollection(dict);
var config = builder.Build();

var settings = new AppSettings();


config.GetSection("App").Bind(settings);

Assert.Equal("Rick", settings.Profile.Machine);
Assert.Equal(11, settings.Window.Height);
Assert.Equal(11, settings.Window.Width);
Assert.Equal("connectionstring", settings.Connection.Value);
}

Crear un proveedor personalizado de Entity Framework


En esta sección, se crea un proveedor de configuración básico que lee pares de nombre y valor de una base de
datos que se crea con EF.
Defina una entidad ConfigurationValue para almacenar los valores de configuración en la base de datos:

public class ConfigurationValue


{
public string Id { get; set; }
public string Value { get; set; }
}

Agregue un ConfigurationContext para almacenar y tener acceso a los valores configurados:


public class ConfigurationContext : DbContext
{
public ConfigurationContext(DbContextOptions options) : base(options)
{
}

public DbSet<ConfigurationValue> Values { get; set; }


}

Cree una clase que implemente IConfigurationSource:

using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;

namespace CustomConfigurationProvider
{
public class EFConfigSource : IConfigurationSource
{
private readonly Action<DbContextOptionsBuilder> _optionsAction;

public EFConfigSource(Action<DbContextOptionsBuilder> optionsAction)


{
_optionsAction = optionsAction;
}

public IConfigurationProvider Build(IConfigurationBuilder builder)


{
return new EFConfigProvider(_optionsAction);
}
}
}

Cree el proveedor de configuración personalizado heredando de ConfigurationProvider. El proveedor de


configuración inicializa la base de datos cuando está vacía:
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;

namespace CustomConfigurationProvider
{
public class EFConfigProvider : ConfigurationProvider
{
public EFConfigProvider(Action<DbContextOptionsBuilder> optionsAction)
{
OptionsAction = optionsAction;
}

Action<DbContextOptionsBuilder> OptionsAction { get; }

// Load config data from EF DB.


public override void Load()
{
var builder = new DbContextOptionsBuilder<ConfigurationContext>();
OptionsAction(builder);

using (var dbContext = new ConfigurationContext(builder.Options))


{
dbContext.Database.EnsureCreated();
Data = !dbContext.Values.Any()
? CreateAndSaveDefaultValues(dbContext)
: dbContext.Values.ToDictionary(c => c.Id, c => c.Value);
}
}

private static IDictionary<string, string> CreateAndSaveDefaultValues(


ConfigurationContext dbContext)
{
var configValues = new Dictionary<string, string>
{
{ "key1", "value_from_ef_1" },
{ "key2", "value_from_ef_2" }
};
dbContext.Values.AddRange(configValues
.Select(kvp => new ConfigurationValue { Id = kvp.Key, Value = kvp.Value })
.ToArray());
dbContext.SaveChanges();
return configValues;
}
}
}

Cuando se ejecuta el ejemplo, se muestran los valores resaltados de la base de datos ("value_from_ef_1" y
"value_from_ef_2").
Se puede usar un método de extensión EFConfigSource para agregar el origen de configuración:
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;

namespace CustomConfigurationProvider
{
public static class EntityFrameworkExtensions
{
public static IConfigurationBuilder AddEntityFrameworkConfig(
this IConfigurationBuilder builder, Action<DbContextOptionsBuilder> setup)
{
return builder.Add(new EFConfigSource(setup));
}
}
}

En el código siguiente se muestra cómo puede usar el EFConfigProvider personalizado:

using System;
using System.IO;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using CustomConfigurationProvider;

public static class Program


{
public static void Main()
{
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json");

var connectionStringConfig = builder.Build();

var config = new ConfigurationBuilder()


.SetBasePath(Directory.GetCurrentDirectory())
// Add "appsettings.json" to bootstrap EF config.
.AddJsonFile("appsettings.json")
// Add the EF configuration provider, which will override any
// config made with the JSON provider.
.AddEntityFrameworkConfig(options =>
options.UseSqlServer(connectionStringConfig.GetConnectionString(
"DefaultConnection"))
)
.Build();

Console.WriteLine("key1={0}", config["key1"]);
Console.WriteLine("key2={0}", config["key2"]);
Console.WriteLine("key3={0}", config["key3"]);
Console.WriteLine();

Console.WriteLine("Press a key...");
Console.ReadKey();
}
}

Tenga en cuenta que el ejemplo agrega el EFConfigProvider personalizado después del proveedor de JSON, por lo
que cualquier configuración de la base de datos invalidará la configuración del archivo appsettings.json.
Con el siguiente archivo appsettings.json:
{
"ConnectionStrings": {
"DefaultConnection": "Server=
(localdb)\\mssqllocaldb;Database=CustomConfigurationProvider;Trusted_Connection=True;MultipleActiveResultSets=t
rue"
},
"key1": "value_from_json_1",
"key2": "value_from_json_2",
"key3": "value_from_json_3"
}

Se muestra el siguiente resultado:

key1=value_from_ef_1
key2=value_from_ef_2
key3=value_from_json_3

Proveedor de configuración CommandLine


El proveedor de configuración CommandLine recibe pares de clave y valor de argumento de línea de comandos
para la configuración en tiempo de ejecución.
Ver o descargar el ejemplo de configuración CommandLine
Configurar y usar el proveedor de configuración CommandLine
Configuración básica
ASP.NET Core 2.x
ASP.NET Core 1.x
Para activar la configuración de línea de comandos, llame al método de extensión AddCommandLine en una instancia
de ConfigurationBuilder:
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Configuration;

public class Program


{
public static IConfiguration Configuration { get; set; }

public static void Main(string[] args = null)


{
var dict = new Dictionary<string, string>
{
{"Profile:MachineName", "MairaPC"},
{"App:MainWindow:Left", "1980"}
};

var builder = new ConfigurationBuilder();

builder.AddInMemoryCollection(dict)
.AddCommandLine(args);

Configuration = builder.Build();

Console.WriteLine($"MachineName: {Configuration["Profile:MachineName"]}");
Console.WriteLine($"Left: {Configuration["App:MainWindow:Left"]}");
Console.WriteLine();

Console.WriteLine("Press a key...");
Console.ReadKey();
}
}

Al ejecutar el código, se muestra la salida siguiente:

MachineName: MairaPC
Left: 1980

Pasar pares de clave y valor de argumento en la línea de comandos cambia los valores de Profile:MachineName y
App:MainWindow:Left :

dotnet run Profile:MachineName=BartPC App:MainWindow:Left=1979

En la ventana de consola se muestra lo siguiente:

MachineName: BartPC
Left: 1979

Para invalidar la configuración proporcionada por otros proveedores de configuración con la configuración de línea
de comandos, llame a AddCommandLine en último lugar en ConfigurationBuilder :

var config = new ConfigurationBuilder()


.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddEnvironmentVariables()
.AddCommandLine(args)
.Build();
Argumentos
Los argumentos que se pasan en la línea de comandos deben ajustarse a uno de los dos formatos que se muestran
en la tabla siguiente:

FORMATO DE ARGUMENTO EJEMPLO

Un solo argumento: un par de clave y valor separado por un key1=value


signo igual ( = )

Secuencia de dos argumentos: un par de clave y valor /key1 value1


separado por un espacio

Un solo argumento
El valor debe seguir a un signo igual ( = ). El valor puede ser NULL (por ejemplo, mykey= ).
La clave puede tener un prefijo.

PREFIJO DE LA CLAVE EJEMPLO

Sin prefijo key1=value1

Un solo guion ( - )† -key2=value2

Dos guiones ( -- ) --key3=value3

Barra diagonal ( / ) /key4=value4

†En las asignaciones de modificador que se describen a continuación debe proporcionarse una clave con un prefijo
de un solo guion ( - ).
Comando de ejemplo:

dotnet run key1=value1 -key2=value2 --key3=value3 /key4=value4

Nota: Si -key2 no está presente en las asignaciones de modificador que se proporcionan al proveedor de
configuración, se produce una excepción FormatException .
Secuencia de dos argumentos
El valor no puede ser NULL y debe seguir a la clave separado por un espacio.
La clave debe tener un prefijo.

PREFIJO DE LA CLAVE EJEMPLO

Un solo guion ( - )† -key1 value1

Dos guiones ( -- ) --key2 value2

Barra diagonal ( / ) /key3 value3

†En las asignaciones de modificador que se describen a continuación debe proporcionarse una clave con un prefijo
de un solo guion ( - ).
Comando de ejemplo:

dotnet run -key1 value1 --key2 value2 /key3 value3

Nota: Si -key1 no está presente en las asignaciones de modificador que se proporcionan al proveedor de
configuración, se produce una excepción FormatException .
Claves duplicadas
Si se proporcionan claves duplicadas, se usa el último par de clave y valor.
Asignaciones de modificador
Si realiza la configuración de compilación manualmente con ConfigurationBuilder , se puede agregar un
diccionario de asignaciones de modificador al método AddCommandLine . Las asignaciones de modificador admiten la
lógica de sustitución de nombres de clave.
Cuando se usa el diccionario de asignaciones de modificador, se comprueba en el diccionario si una clave coincide
con la clave proporcionada por un argumento de línea de comandos. Si la clave de la línea de comandos se
encuentra en el diccionario, se devuelve el valor del diccionario (el reemplazo de la clave) para establecer la
configuración. Se requiere una asignación de conmutador para cualquier clave de línea de comandos precedida por
un solo guion ( - ).
Reglas de clave del diccionario de asignaciones de modificador:
Los modificadores deben empezar por un guion ( - ) o guion doble ( -- ).
El diccionario de asignaciones de modificador no debe contener claves duplicadas.
En el ejemplo siguiente, el método GetSwitchMappings permite que los argumentos de línea de comandos usen un
prefijo de clave de un solo guión ( - ) y evita prefijos de subclave iniciales.
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Configuration;

public class Program


{
public static IConfiguration Configuration { get; set; }

public static Dictionary<string, string> GetSwitchMappings(


IReadOnlyDictionary<string, string> configurationStrings)
{
return configurationStrings.Select(item =>
new KeyValuePair<string, string>(
"-" + item.Key.Substring(item.Key.LastIndexOf(':') + 1),
item.Key))
.ToDictionary(
item => item.Key, item => item.Value);
}

public static void Main(string[] args = null)


{
var dict = new Dictionary<string, string>
{
{"Profile:MachineName", "RickPC"},
{"App:MainWindow:Left", "1980"}
};

var builder = new ConfigurationBuilder();

builder.AddInMemoryCollection(dict)
.AddCommandLine(args, GetSwitchMappings(dict));

Configuration = builder.Build();

Console.WriteLine($"MachineName: {Configuration["Profile:MachineName"]}");
Console.WriteLine($"Left: {Configuration["App:MainWindow:Left"]}");
Console.WriteLine();

Console.WriteLine("Press a key...");
Console.ReadKey();
}
}

Sin proporcionar argumentos de línea de comandos, el diccionario proporcionado para AddInMemoryCollection


establece los valores de configuración. Ejecute la aplicación con el comando siguiente:

dotnet run

En la ventana de consola se muestra lo siguiente:

MachineName: RickPC
Left: 1980

Use lo siguiente para pasar valores de configuración:

dotnet run /Profile:MachineName=DahliaPC /App:MainWindow:Left=1984

En la ventana de consola se muestra lo siguiente:


MachineName: DahliaPC
Left: 1984

Después de crear el diccionario de asignaciones de modificador, contiene los datos que se muestran en la tabla
siguiente:

KEY VALOR

-MachineName Profile:MachineName

-Left App:MainWindow:Left

Para mostrar la conmutación de claves mediante el diccionario, ejecute el comando siguiente:

dotnet run -MachineName=ChadPC -Left=1988

Se intercambian las claves de la línea de comandos. En la ventana de consola se muestran los valores de
configuración de Profile:MachineName y App:MainWindow:Left :

MachineName: ChadPC
Left: 1988

Archivo web.config
Un archivo web.config es necesario cuando la aplicación se hospeda en IIS o IIS Express. La configuración de
web.config habilita el módulo ASP.NET Core para que inicie la aplicación y configure otros módulos y valores de
configuración de IIS. Si el archivo web.config no está presente y el archivo de proyecto incluye
<Project Sdk="Microsoft.NET.Sdk.Web"> , al publicar el proyecto se crea un archivo web.config en la salida publicada
(la carpeta de publicación). Para más información, vea Host ASP.NET Core on Windows with IIS (Hospedar
ASP.NET Core en Windows con IIS ).

Acceso a la configuración durante el inicio


Para acceder a la configuración en ConfigureServices o Configure durante el inicio, vea los ejemplos del tema
Inicio de la aplicación.

Agregar opciones de configuración a partir de un ensamblado externo


Una implementación de IHostingStartup permite agregar mejoras a una aplicación al iniciarla a partir de un
ensamblado externo fuera de la clase Startup de esta. Para obtener más información, consulte Mejora de una
aplicación a partir de un ensamblado externo.

Acceso a la configuración en una página de Razor o en una vista de


MVC
Para obtener acceso a los valores de configuración en una página de las páginas de Razor o una vista de MVC,
agregue una directiva using (referencia de C#: directiva using) para el espacio de nombres
Microsoft.Extensions.Configuration e inyecte IConfiguration en la página o la vista.
En una página de las páginas de Razor:
@page
@model IndexModel

@using Microsoft.Extensions.Configuration
@inject IConfiguration Configuration

<!DOCTYPE html>
<html lang="en">
<head>
<title>Index Page</title>
</head>
<body>
<h1>Access configuration in a Razor Pages page</h1>
<p>Configuration[&quot;key&quot;]: @Configuration["key"]</p>
</body>
</html>

En una vista de MVC:

@using Microsoft.Extensions.Configuration
@inject IConfiguration Configuration

<!DOCTYPE html>
<html lang="en">
<head>
<title>Index View</title>
</head>
<body>
<h1>Access configuration in an MVC view</h1>
<p>Configuration[&quot;key&quot;]: @Configuration["key"]</p>
</body>
</html>

Notas adicionales
La inserción de dependencias (DI) no se establece hasta que se invoca ConfigureServices .
El sistema de configuración no es compatible con DI.
IConfiguration tiene dos especializaciones:
IConfigurationRoot Se usa para el nodo raíz. Puede desencadenar una recarga.
IConfigurationSection Representa una sección de valores de configuración. Los métodos GetSection y
GetChildren devuelven un elemento IConfigurationSection .
Use IConfigurationRoot al recargar la configuración o para acceder a todos los proveedores. Ninguna de
estas situaciones son comunes.

Recursos adicionales
Opciones
Uso de varios entornos
Almacenamiento seguro de secretos de aplicación en el desarrollo
Hospedaje en ASP.NET Core
Inserción de dependencias
Proveedor de configuración de Azure Key Vault
Configuración en ASP.NET Core
21/06/2018 • 27 minutes to read • Edit Online

Por Rick Anderson, Mark Michaelis, Steve Smith, Daniel Roth y Luke Latham
La API de configuración proporciona una manera de configurar una aplicación web ASP.NET Core
según una lista de pares de nombre y valor. La configuración se lee en tiempo de ejecución desde varios
orígenes. Puede agrupar estos pares nombre-valor en una jerarquía multinivel.
Existen proveedores de configuración para:
Formatos de archivo (INI, JSON y XML ).
Argumentos de la línea de comandos.
Variables de entorno.
Objetos de .NET en memoria.
El almacenamiento de administrador secreto sin cifrar.
Un almacén de usuario cifrado, como Azure Key Vault.
Proveedores personalizados (instalados o creados).
Cada valor de configuración se asigna a una clave de cadena. Hay compatibilidad de enlace integrada
para deserializar la configuración en un objeto POCO personalizado (una clase simple de .NET con
propiedades).
El patrón de opciones usa las clases de opciones para representar grupos de configuraciones
relacionadas. Para más información sobre cómo usar el patrón de opciones, vea el tema Opciones.
Vea o descargue el código de ejemplo (cómo descargarlo)

Configuración de JSON
En la aplicación de consola siguiente se usa el proveedor de configuración de JSON:
using System;
using System.IO;
// Requires NuGet package
// Microsoft.Extensions.Configuration.Json
using Microsoft.Extensions.Configuration;

public class Program


{
public static IConfiguration Configuration { get; set; }

public static void Main(string[] args = null)


{
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json");

Configuration = builder.Build();

Console.WriteLine($"option1 = {Configuration["Option1"]}");
Console.WriteLine($"option2 = {Configuration["option2"]}");
Console.WriteLine(
$"suboption1 = {Configuration["subsection:suboption1"]}");
Console.WriteLine();

Console.WriteLine("Wizards:");
Console.Write($"{Configuration["wizards:0:Name"]}, ");
Console.WriteLine($"age {Configuration["wizards:0:Age"]}");
Console.Write($"{Configuration["wizards:1:Name"]}, ");
Console.WriteLine($"age {Configuration["wizards:1:Age"]}");
Console.WriteLine();

Console.WriteLine("Press a key...");
Console.ReadKey();
}
}

La aplicación lee y muestra los valores de configuración siguientes:

{
"option1": "value1_from_json",
"option2": 2,

"subsection": {
"suboption1": "subvalue1_from_json"
},
"wizards": [
{
"Name": "Gandalf",
"Age": "1000"
},
{
"Name": "Harry",
"Age": "17"
}
]
}

La configuración consta de una lista jerárquica de pares de nombre y valor en los que los nodos se
separan con dos puntos ( : ). Para recuperar un valor, obtenga acceso al indizador Configuration con la
clave del elemento correspondiente:
Console.WriteLine(
$"suboption1 = {Configuration["subsection:suboption1"]}");

Para trabajar con matrices en orígenes de configuración con formato JSON, use un índice de matriz
como parte de la cadena separada por dos puntos. En el ejemplo siguiente se obtiene el nombre del
primer elemento de la matriz wizards anterior:

Console.Write($"{Configuration["wizards:0:Name"]}");
// Output: Gandalf

Los pares de nombre y valor que se escriben en los proveedores de Configuración integrados no se
conservan, pero se puede crear un proveedor personalizado que guarde los valores. Vea Proveedor de
configuración personalizado.
En el ejemplo anterior se usa el indizador de configuración para leer los valores. Para obtener acceso a
la configuración fuera de Startup , use el patrón de opciones. Para más información, vea el tema
Opciones.

Configuración XML
Para trabajar con matrices en orígenes de configuración con formato XML, proporcione un índice name
a cada elemento. Use el índice para acceder a los valores:

<wizards>
<wizard name="Gandalf">
<age>1000</age>
</wizard>
<wizard name="Harry">
<age>17</age>
</wizard>
</wizards>

Console.Write($"{Configuration["wizard:Harry:age"]}");
// Output: 17

Configuración de entorno
Es habitual tener distintos valores de configuración para entornos diferentes, por ejemplo para
desarrollo, pruebas y producción. El método de extensión CreateDefaultBuilder en una aplicación de
ASP.NET Core 2.x (o mediante AddJsonFile y AddEnvironmentVariables directamente en una aplicación
de ASP.NET Core 1.x) agrega proveedores de configuración para leer archivos JSON y orígenes de
configuración del sistema:
appsettings.json
appsettings.<NombreDelEntorno>.json
Variables de entorno
Las aplicaciones ASP.NET Core 1.x deben llamar a AddJsonFile y AddEnvironmentVariables.
Vea AddJsonFile para obtener una explicación de los parámetros. reloadOnChange solo se admite en
ASP.NET Core 1.1 y versiones posteriores.
Los orígenes de configuración se leen en el orden en que se especifican. En el código anterior, las
variables de entorno se leen en último lugar. Cualquier valor de configuración establecido mediante el
entorno reemplaza a los establecidos en los dos proveedores anteriores.
Tenga en cuenta el siguiente archivo appsettings.Staging.json:

{
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"System": "Information",
"Microsoft": "Information"
}
},
"MyConfig": "My Config Value for staging."
}

En el siguiente código, Configure lee el valor de MyConfig :

public void Configure(IApplicationBuilder app, IHostingEnvironment env)


{
var myConfig = Configuration["MyConfig"];
// use myConfig
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}

if (env.IsProduction() || env.IsStaging())
{
app.UseExceptionHandler("/Error");
}

app.UseStaticFiles();
app.UseMvcWithDefaultRoute();
}

El entorno se establece normalmente en Development , Staging o Production . Para obtener más


información, consulte Uso de varios entornos.
Consideraciones de configuración:
IOptionsSnapshot puede volver a cargar los datos de configuración cuando cambia.
En las claves de configuraciones no se distingue entre mayúsculas y minúsculas.
Nunca almacene contraseñas u otros datos confidenciales en el código del proveedor de
configuración o en archivos de configuración de texto sin formato. No use secretos de producción en
los entornos de desarrollo o pruebas. Especifique los secretos fuera del proyecto para que no se
confirmen en un repositorio de código fuente de manera accidental. Obtenga más información
sobre cómo usar con varios entornos y administrar el almacenamiento seguro de los secretos de
aplicación en el desarrollo.
Para los valores de configuración jerárquica especificados en las variables de entorno, el signo de
dos puntos ( : ) podría no funcionar en todas las plataformas. El guion bajo doble ( __ ) es
compatible con todas las plataformas.
Al interactuar con la API de configuración, el signo de dos puntos ( : ) funciona en todas las
plataformas.

Proveedor en memoria y enlace a una clase POCO


En el ejemplo siguiente se muestra cómo usar el proveedor en memoria y enlazar a una clase:

using System;
using System.Collections.Generic;
using Microsoft.Extensions.Configuration;

public class Program


{
public static IConfiguration Configuration { get; set; }

public static void Main(string[] args = null)


{
var dict = new Dictionary<string, string>
{
{"Profile:MachineName", "Rick"},
{"App:MainWindow:Height", "11"},
{"App:MainWindow:Width", "11"},
{"App:MainWindow:Top", "11"},
{"App:MainWindow:Left", "11"}
};

var builder = new ConfigurationBuilder();


builder.AddInMemoryCollection(dict);

Configuration = builder.Build();

Console.WriteLine($"Hello {Configuration["Profile:MachineName"]}");

var window = new MyWindow();


// Bind requrires NuGet package
// Microsoft.Extensions.Configuration.Binder
Configuration.GetSection("App:MainWindow").Bind(window);
Console.WriteLine($"Left {window.Left}");
Console.WriteLine();

Console.WriteLine("Press any key...");


Console.ReadKey();
}
}

public class MyWindow


{
public int Height { get; set; }
public int Width { get; set; }
public int Top { get; set; }
public int Left { get; set; }
}

Los valores de configuración se devuelven como cadenas, pero el enlace permite la construcción de
objetos. El enlace permite recuperar objetos POCO o incluso gráficos de objetos completos.
GetValue
En el ejemplo siguiente se muestra el método de extensión GetValue<T>:
using System;
using System.Collections.Generic;
using Microsoft.Extensions.Configuration;

public class Program


{
public static IConfiguration Configuration { get; set; }

public static void Main(string[] args = null)


{
var dict = new Dictionary<string, string>
{
{"Profile:MachineName", "Rick"},
{"App:MainWindow:Height", "11"},
{"App:MainWindow:Width", "11"},
{"App:MainWindow:Top", "11"},
{"App:MainWindow:Left", "11"}
};

var builder = new ConfigurationBuilder();


builder.AddInMemoryCollection(dict);

Configuration = builder.Build();

Console.WriteLine($"Hello {Configuration["Profile:MachineName"]}");

// Show GetValue overload and set the default value to 80


// Requires NuGet package "Microsoft.Extensions.Configuration.Binder"
var left = Configuration.GetValue<int>("App:MainWindow:Left", 80);
Console.WriteLine($"Left {left}");

var window = new MyWindow();


Configuration.GetSection("App:MainWindow").Bind(window);
Console.WriteLine($"Left {window.Left}");
Console.WriteLine();

Console.WriteLine("Press a key...");
Console.ReadKey();
}
}

public class MyWindow


{
public int Height { get; set; }
public int Width { get; set; }
public int Top { get; set; }
public int Left { get; set; }
}

El método GetValue<T> de ConfigurationBinder permite especificar un valor predeterminado (80 en el


ejemplo). GetValue<T> es para escenarios sencillos y no se enlaza con secciones completas.
GetValue<T> obtiene los valores escalares de GetSection(key).Value convertidos a un tipo específico.

Enlazar a un gráfico de objetos


Cada objeto de una clase se puede enlazar de forma recursiva. Observe la clase AppSettings siguiente:
public class AppSettings
{
public Window Window { get; set; }
public Connection Connection { get; set; }
public Profile Profile { get; set; }
}

public class Window


{
public int Height { get; set; }
public int Width { get; set; }
}

public class Connection


{
public string Value { get; set; }
}

public class Profile


{
public string Machine { get; set; }
}

El ejemplo siguiente se enlaza a la clase AppSettings :

using System;
using System.IO;
using Microsoft.Extensions.Configuration;

public class Program


{
public static void Main(string[] args = null)
{
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json");

var config = builder.Build();

var appConfig = new AppSettings();


config.GetSection("App").Bind(appConfig);

Console.WriteLine($"Height {appConfig.Window.Height}");
Console.WriteLine();

Console.WriteLine("Press a key...");
Console.ReadKey();
}
}

En ASP.NET Core 1.1 y versiones posteriores se puede usar Get<T> , que funciona con secciones
completas. Get<T> puede ser más conveniente que usar Bind . En el código siguiente se muestra cómo
usar Get<T> con el ejemplo anterior:

var appConfig = config.GetSection("App").Get<AppSettings>();

Con el siguiente archivo appsettings.json:


{
"App": {
"Profile": {
"Machine": "Rick"
},
"Connection": {
"Value": "connectionstring"
},
"Window": {
"Height": "11",
"Width": "11"
}
}
}

El programa muestra Height 11 .


El código siguiente se puede usar para pruebas unitarias de la configuración:

[Fact]
public void CanBindObjectTree()
{
var dict = new Dictionary<string, string>
{
{"App:Profile:Machine", "Rick"},
{"App:Connection:Value", "connectionstring"},
{"App:Window:Height", "11"},
{"App:Window:Width", "11"}
};
var builder = new ConfigurationBuilder();
builder.AddInMemoryCollection(dict);
var config = builder.Build();

var settings = new AppSettings();


config.GetSection("App").Bind(settings);

Assert.Equal("Rick", settings.Profile.Machine);
Assert.Equal(11, settings.Window.Height);
Assert.Equal(11, settings.Window.Width);
Assert.Equal("connectionstring", settings.Connection.Value);
}

Crear un proveedor personalizado de Entity Framework


En esta sección, se crea un proveedor de configuración básico que lee pares de nombre y valor de una
base de datos que se crea con EF.
Defina una entidad ConfigurationValue para almacenar los valores de configuración en la base de
datos:

public class ConfigurationValue


{
public string Id { get; set; }
public string Value { get; set; }
}

Agregue un ConfigurationContext para almacenar y tener acceso a los valores configurados:


public class ConfigurationContext : DbContext
{
public ConfigurationContext(DbContextOptions options) : base(options)
{
}

public DbSet<ConfigurationValue> Values { get; set; }


}

Cree una clase que implemente IConfigurationSource:

using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;

namespace CustomConfigurationProvider
{
public class EFConfigSource : IConfigurationSource
{
private readonly Action<DbContextOptionsBuilder> _optionsAction;

public EFConfigSource(Action<DbContextOptionsBuilder> optionsAction)


{
_optionsAction = optionsAction;
}

public IConfigurationProvider Build(IConfigurationBuilder builder)


{
return new EFConfigProvider(_optionsAction);
}
}
}

Cree el proveedor de configuración personalizado heredando de ConfigurationProvider. El proveedor


de configuración inicializa la base de datos cuando está vacía:
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;

namespace CustomConfigurationProvider
{
public class EFConfigProvider : ConfigurationProvider
{
public EFConfigProvider(Action<DbContextOptionsBuilder> optionsAction)
{
OptionsAction = optionsAction;
}

Action<DbContextOptionsBuilder> OptionsAction { get; }

// Load config data from EF DB.


public override void Load()
{
var builder = new DbContextOptionsBuilder<ConfigurationContext>();
OptionsAction(builder);

using (var dbContext = new ConfigurationContext(builder.Options))


{
dbContext.Database.EnsureCreated();
Data = !dbContext.Values.Any()
? CreateAndSaveDefaultValues(dbContext)
: dbContext.Values.ToDictionary(c => c.Id, c => c.Value);
}
}

private static IDictionary<string, string> CreateAndSaveDefaultValues(


ConfigurationContext dbContext)
{
var configValues = new Dictionary<string, string>
{
{ "key1", "value_from_ef_1" },
{ "key2", "value_from_ef_2" }
};
dbContext.Values.AddRange(configValues
.Select(kvp => new ConfigurationValue { Id = kvp.Key, Value = kvp.Value })
.ToArray());
dbContext.SaveChanges();
return configValues;
}
}
}

Cuando se ejecuta el ejemplo, se muestran los valores resaltados de la base de datos


("value_from_ef_1" y "value_from_ef_2").
Se puede usar un método de extensión EFConfigSource para agregar el origen de configuración:
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;

namespace CustomConfigurationProvider
{
public static class EntityFrameworkExtensions
{
public static IConfigurationBuilder AddEntityFrameworkConfig(
this IConfigurationBuilder builder, Action<DbContextOptionsBuilder> setup)
{
return builder.Add(new EFConfigSource(setup));
}
}
}

En el código siguiente se muestra cómo puede usar el EFConfigProvider personalizado:

using System;
using System.IO;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using CustomConfigurationProvider;

public static class Program


{
public static void Main()
{
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json");

var connectionStringConfig = builder.Build();

var config = new ConfigurationBuilder()


.SetBasePath(Directory.GetCurrentDirectory())
// Add "appsettings.json" to bootstrap EF config.
.AddJsonFile("appsettings.json")
// Add the EF configuration provider, which will override any
// config made with the JSON provider.
.AddEntityFrameworkConfig(options =>
options.UseSqlServer(connectionStringConfig.GetConnectionString(
"DefaultConnection"))
)
.Build();

Console.WriteLine("key1={0}", config["key1"]);
Console.WriteLine("key2={0}", config["key2"]);
Console.WriteLine("key3={0}", config["key3"]);
Console.WriteLine();

Console.WriteLine("Press a key...");
Console.ReadKey();
}
}

Tenga en cuenta que el ejemplo agrega el EFConfigProvider personalizado después del proveedor de
JSON, por lo que cualquier configuración de la base de datos invalidará la configuración del archivo
appsettings.json.
Con el siguiente archivo appsettings.json:
{
"ConnectionStrings": {
"DefaultConnection": "Server=
(localdb)\\mssqllocaldb;Database=CustomConfigurationProvider;Trusted_Connection=True;MultipleActive
ResultSets=true"
},
"key1": "value_from_json_1",
"key2": "value_from_json_2",
"key3": "value_from_json_3"
}

Se muestra el siguiente resultado:

key1=value_from_ef_1
key2=value_from_ef_2
key3=value_from_json_3

Proveedor de configuración CommandLine


El proveedor de configuración CommandLine recibe pares de clave y valor de argumento de línea de
comandos para la configuración en tiempo de ejecución.
Ver o descargar el ejemplo de configuración CommandLine
Configurar y usar el proveedor de configuración CommandLine
Configuración básica
ASP.NET Core 2.x
ASP.NET Core 1.x
Para activar la configuración de línea de comandos, llame al método de extensión AddCommandLine en
una instancia de ConfigurationBuilder:
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Configuration;

public class Program


{
public static IConfiguration Configuration { get; set; }

public static void Main(string[] args = null)


{
var dict = new Dictionary<string, string>
{
{"Profile:MachineName", "MairaPC"},
{"App:MainWindow:Left", "1980"}
};

var builder = new ConfigurationBuilder();

builder.AddInMemoryCollection(dict)
.AddCommandLine(args);

Configuration = builder.Build();

Console.WriteLine($"MachineName: {Configuration["Profile:MachineName"]}");
Console.WriteLine($"Left: {Configuration["App:MainWindow:Left"]}");
Console.WriteLine();

Console.WriteLine("Press a key...");
Console.ReadKey();
}
}

Al ejecutar el código, se muestra la salida siguiente:

MachineName: MairaPC
Left: 1980

Pasar pares de clave y valor de argumento en la línea de comandos cambia los valores de
Profile:MachineName y App:MainWindow:Left :

dotnet run Profile:MachineName=BartPC App:MainWindow:Left=1979

En la ventana de consola se muestra lo siguiente:

MachineName: BartPC
Left: 1979

Para invalidar la configuración proporcionada por otros proveedores de configuración con la


configuración de línea de comandos, llame a AddCommandLine en último lugar en ConfigurationBuilder :

var config = new ConfigurationBuilder()


.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddEnvironmentVariables()
.AddCommandLine(args)
.Build();
Argumentos
Los argumentos que se pasan en la línea de comandos deben ajustarse a uno de los dos formatos que
se muestran en la tabla siguiente:

FORMATO DE ARGUMENTO EJEMPLO

Un solo argumento: un par de clave y valor separado key1=value


por un signo igual ( = )

Secuencia de dos argumentos: un par de clave y valor /key1 value1


separado por un espacio

Un solo argumento
El valor debe seguir a un signo igual ( = ). El valor puede ser NULL (por ejemplo, mykey= ).
La clave puede tener un prefijo.

PREFIJO DE LA CLAVE EJEMPLO

Sin prefijo key1=value1

Un solo guion ( - )† -key2=value2

Dos guiones ( -- ) --key3=value3

Barra diagonal ( / ) /key4=value4

†En las asignaciones de modificador que se describen a continuación debe proporcionarse una clave
con un prefijo de un solo guion ( - ).
Comando de ejemplo:

dotnet run key1=value1 -key2=value2 --key3=value3 /key4=value4

Nota: Si -key2 no está presente en las asignaciones de modificador que se proporcionan al proveedor
de configuración, se produce una excepción FormatException .
Secuencia de dos argumentos
El valor no puede ser NULL y debe seguir a la clave separado por un espacio.
La clave debe tener un prefijo.

PREFIJO DE LA CLAVE EJEMPLO

Un solo guion ( - )† -key1 value1

Dos guiones ( -- ) --key2 value2

Barra diagonal ( / ) /key3 value3

†En las asignaciones de modificador que se describen a continuación debe proporcionarse una clave
con un prefijo de un solo guion ( - ).
Comando de ejemplo:

dotnet run -key1 value1 --key2 value2 /key3 value3

Nota: Si -key1 no está presente en las asignaciones de modificador que se proporcionan al proveedor
de configuración, se produce una excepción FormatException .
Claves duplicadas
Si se proporcionan claves duplicadas, se usa el último par de clave y valor.
Asignaciones de modificador
Si realiza la configuración de compilación manualmente con ConfigurationBuilder , se puede agregar
un diccionario de asignaciones de modificador al método AddCommandLine . Las asignaciones de
modificador admiten la lógica de sustitución de nombres de clave.
Cuando se usa el diccionario de asignaciones de modificador, se comprueba en el diccionario si una
clave coincide con la clave proporcionada por un argumento de línea de comandos. Si la clave de la
línea de comandos se encuentra en el diccionario, se devuelve el valor del diccionario (el reemplazo de
la clave) para establecer la configuración. Se requiere una asignación de conmutador para cualquier
clave de línea de comandos precedida por un solo guion ( - ).
Reglas de clave del diccionario de asignaciones de modificador:
Los modificadores deben empezar por un guion ( - ) o guion doble ( -- ).
El diccionario de asignaciones de modificador no debe contener claves duplicadas.
En el ejemplo siguiente, el método GetSwitchMappings permite que los argumentos de línea de
comandos usen un prefijo de clave de un solo guión ( - ) y evita prefijos de subclave iniciales.
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Configuration;

public class Program


{
public static IConfiguration Configuration { get; set; }

public static Dictionary<string, string> GetSwitchMappings(


IReadOnlyDictionary<string, string> configurationStrings)
{
return configurationStrings.Select(item =>
new KeyValuePair<string, string>(
"-" + item.Key.Substring(item.Key.LastIndexOf(':') + 1),
item.Key))
.ToDictionary(
item => item.Key, item => item.Value);
}

public static void Main(string[] args = null)


{
var dict = new Dictionary<string, string>
{
{"Profile:MachineName", "RickPC"},
{"App:MainWindow:Left", "1980"}
};

var builder = new ConfigurationBuilder();

builder.AddInMemoryCollection(dict)
.AddCommandLine(args, GetSwitchMappings(dict));

Configuration = builder.Build();

Console.WriteLine($"MachineName: {Configuration["Profile:MachineName"]}");
Console.WriteLine($"Left: {Configuration["App:MainWindow:Left"]}");
Console.WriteLine();

Console.WriteLine("Press a key...");
Console.ReadKey();
}
}

Sin proporcionar argumentos de línea de comandos, el diccionario proporcionado para


AddInMemoryCollection establece los valores de configuración. Ejecute la aplicación con el comando
siguiente:

dotnet run

En la ventana de consola se muestra lo siguiente:

MachineName: RickPC
Left: 1980

Use lo siguiente para pasar valores de configuración:

dotnet run /Profile:MachineName=DahliaPC /App:MainWindow:Left=1984

En la ventana de consola se muestra lo siguiente:


MachineName: DahliaPC
Left: 1984

Después de crear el diccionario de asignaciones de modificador, contiene los datos que se muestran en
la tabla siguiente:

KEY VALOR

-MachineName Profile:MachineName

-Left App:MainWindow:Left

Para mostrar la conmutación de claves mediante el diccionario, ejecute el comando siguiente:

dotnet run -MachineName=ChadPC -Left=1988

Se intercambian las claves de la línea de comandos. En la ventana de consola se muestran los valores
de configuración de Profile:MachineName y App:MainWindow:Left :

MachineName: ChadPC
Left: 1988

Archivo web.config
Un archivo web.config es necesario cuando la aplicación se hospeda en IIS o IIS Express. La
configuración de web.config habilita el módulo ASP.NET Core para que inicie la aplicación y configure
otros módulos y valores de configuración de IIS. Si el archivo web.config no está presente y el archivo
de proyecto incluye <Project Sdk="Microsoft.NET.Sdk.Web"> , al publicar el proyecto se crea un archivo
web.config en la salida publicada (la carpeta de publicación). Para más información, vea Host ASP.NET
Core on Windows with IIS (Hospedar ASP.NET Core en Windows con IIS ).

Acceso a la configuración durante el inicio


Para acceder a la configuración en ConfigureServices o Configure durante el inicio, vea los ejemplos
del tema Inicio de la aplicación.

Agregar opciones de configuración a partir de un ensamblado


externo
Una implementación de IHostingStartup permite agregar mejoras a una aplicación al iniciarla a partir
de un ensamblado externo fuera de la clase Startup de esta. Para obtener más información, consulte
Mejora de una aplicación a partir de un ensamblado externo.

Acceso a la configuración en una página de Razor o en una vista


de MVC
Para obtener acceso a los valores de configuración en una página de las páginas de Razor o una vista
de MVC, agregue una directiva using (referencia de C#: directiva using) para el espacio de nombres
Microsoft.Extensions.Configuration e inyecte IConfiguration en la página o la vista.
En una página de las páginas de Razor:
@page
@model IndexModel

@using Microsoft.Extensions.Configuration
@inject IConfiguration Configuration

<!DOCTYPE html>
<html lang="en">
<head>
<title>Index Page</title>
</head>
<body>
<h1>Access configuration in a Razor Pages page</h1>
<p>Configuration[&quot;key&quot;]: @Configuration["key"]</p>
</body>
</html>

En una vista de MVC:

@using Microsoft.Extensions.Configuration
@inject IConfiguration Configuration

<!DOCTYPE html>
<html lang="en">
<head>
<title>Index View</title>
</head>
<body>
<h1>Access configuration in an MVC view</h1>
<p>Configuration[&quot;key&quot;]: @Configuration["key"]</p>
</body>
</html>

Notas adicionales
La inserción de dependencias (DI) no se establece hasta que se invoca ConfigureServices .
El sistema de configuración no es compatible con DI.
IConfiguration tiene dos especializaciones:
IConfigurationRoot Se usa para el nodo raíz. Puede desencadenar una recarga.
IConfigurationSection Representa una sección de valores de configuración. Los métodos
GetSection y GetChildren devuelven un elemento IConfigurationSection .
Use IConfigurationRoot al recargar la configuración o para acceder a todos los proveedores.
Ninguna de estas situaciones son comunes.

Recursos adicionales
Opciones
Uso de varios entornos
Almacenamiento seguro de secretos de aplicación en el desarrollo
Hospedaje en ASP.NET Core
Inserción de dependencias
Proveedor de configuración de Azure Key Vault
Patrón de opciones en ASP.NET Core
25/05/2018 • 17 minutes to read • Edit Online

Por Luke Latham


El patrón de opciones usa clases para representar grupos de configuraciones relacionadas. Cuando los valores
de configuración están aislados por característica en clases independientes, la aplicación se ajusta a dos
principios de ingeniería de software importantes:
El principio de segregación de interfaz (ISP ): las características (clases) que dependen de valores de
configuración dependerán únicamente de los valores de configuración que usen.
Separación de intereses: los valores de configuración para distintos elementos de la aplicación no son
dependientes entre sí ni están emparejados.
Vea o descargue el código de ejemplo (cómo descargarlo). Este artículo es más fácil de seguir con la aplicación
de ejemplo.

Configuración de opciones básicas


La configuración de opciones básicas se muestra en el ejemplo #1 en la aplicación de ejemplo.
Una clase de opciones debe ser no abstracta con un constructor público sin parámetros. La siguiente clase,
MyOptions , tiene dos propiedades: Option1 y Option2 . Configurar los valores predeterminados es opcional,
pero el constructor de clases en el ejemplo siguiente establece el valor predeterminado de Option1 . Option2
tiene un valor predeterminado que se establece al inicializar la propiedad directamente (Models/MyOptions.cs):

public class MyOptions


{
public MyOptions()
{
// Set default value.
Option1 = "value1_from_ctor";
}

public string Option1 { get; set; }


public int Option2 { get; set; } = 5;
}

La clase MyOptions se agrega al contenedor de servicios con Configure<TOptions> y enlaza a la


configuración:

// Example #1: Basic options


// Register the Configuration instance which MyOptions binds against.
services.Configure<MyOptions>(Configuration);

El siguiente modelo de página usa la inserción de dependencias de constructor con IOptions<TOptions> para
acceder a la configuración (Pages/Index.cshtml.cs):

private readonly MyOptions _options;


public IndexModel(
IOptions<MyOptions> optionsAccessor,
IOptions<MyOptionsWithDelegateConfig> optionsAccessorWithDelegateConfig,
IOptions<MySubOptions> subOptionsAccessor,
IOptionsSnapshot<MyOptions> snapshotOptionsAccessor,
IOptionsSnapshot<MyOptions> namedOptionsAccessor)
{
_options = optionsAccessor.Value;
_optionsWithDelegateConfig = optionsAccessorWithDelegateConfig.Value;
_subOptions = subOptionsAccessor.Value;
_snapshotOptions = snapshotOptionsAccessor.Value;
_named_options_1 = namedOptionsAccessor.Get("named_options_1");
_named_options_2 = namedOptionsAccessor.Get("named_options_2");
}

// Example #1: Simple options


var option1 = _options.Option1;
var option2 = _options.Option2;
SimpleOptions = $"option1 = {option1}, option2 = {option2}";

El archivo appSettings.json del ejemplo especifica valores para option1 y option2 :

{
"option1": "value1_from_json",
"option2": -1,
"subsection": {
"suboption1": "subvalue1_from_json",
"suboption2": 200
},
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*"
}

Cuando se ejecuta la aplicación, el método OnGet del modelo de página devuelve una cadena que muestra los
valores de la clase de opción:

option1 = value1_from_json, option2 = -1

Configurar opciones simples con un delegado


La configuración de opciones simples con un delegado se muestra como ejemplo #2 en la aplicación de
ejemplo.
Use un delegado para establecer los valores de opciones. La aplicación de ejemplo usa la clase
MyOptionsWithDelegateConfig ( Models/MyOptionsWithDelegateConfig.cs):
public class MyOptionsWithDelegateConfig
{
public MyOptionsWithDelegateConfig()
{
// Set default value.
Option1 = "value1_from_ctor";
}

public string Option1 { get; set; }


public int Option2 { get; set; } = 5;
}

En el código siguiente, un segundo servicio IConfigureOptions<TOptions> se agrega al contenedor de servicios.


Usa un delegado para configurar el enlace con MyOptionsWithDelegateConfig :

// Example #2: Options bound and configured by a delegate


services.Configure<MyOptionsWithDelegateConfig>(myOptions =>
{
myOptions.Option1 = "value1_configured_by_delegate";
myOptions.Option2 = 500;
});

Index.cshtml.cs:

private readonly MyOptionsWithDelegateConfig _optionsWithDelegateConfig;

public IndexModel(
IOptions<MyOptions> optionsAccessor,
IOptions<MyOptionsWithDelegateConfig> optionsAccessorWithDelegateConfig,
IOptions<MySubOptions> subOptionsAccessor,
IOptionsSnapshot<MyOptions> snapshotOptionsAccessor,
IOptionsSnapshot<MyOptions> namedOptionsAccessor)
{
_options = optionsAccessor.Value;
_optionsWithDelegateConfig = optionsAccessorWithDelegateConfig.Value;
_subOptions = subOptionsAccessor.Value;
_snapshotOptions = snapshotOptionsAccessor.Value;
_named_options_1 = namedOptionsAccessor.Get("named_options_1");
_named_options_2 = namedOptionsAccessor.Get("named_options_2");
}

// Example #2: Options configured by delegate


var delegate_config_option1 = _optionsWithDelegateConfig.Option1;
var delegate_config_option2 = _optionsWithDelegateConfig.Option2;
SimpleOptionsWithDelegateConfig =
$"delegate_option1 = {delegate_config_option1}, " +
$"delegate_option2 = {delegate_config_option2}";

Puede agregar varios proveedores de configuración. Los proveedores de configuración están disponibles en
paquetes de NuGet. Se aplican en el orden en que están registrados.
Cada llamada a Configure<TOptions> agrega un servicio IConfigureOptions<TOptions> al contenedor de
servicios. En el ejemplo anterior, los valores de Option1 y Option2 se especifican en appSettings.json, pero los
valores de Option1 y Option2 se reemplazan por el delegado configurado.
Cuando se habilita más de un servicio de configuración, la última fuente de configuración especificada gana y
establece el valor de configuración. Cuando se ejecuta la aplicación, el método OnGet del modelo de página
devuelve una cadena que muestra los valores de la clase de opción:

delegate_option1 = value1_configured_by_delgate, delegate_option2 = 500

Configuración de subopciones
La configuración de subopciones se muestra en el ejemplo #3 en la aplicación de ejemplo.
Las aplicaciones deben crear clases de opciones que pertenezcan a grupos específicos de características
(clases) en la aplicación. Los elementos de la aplicación que requieran valores de configuración deben acceder
solamente a los valores de configuración que usen.
Al enlazar opciones para la configuración, cada propiedad en el tipo de opciones se enlaza a una clave de
configuración del formulario property[:sub-property:] . Por ejemplo, la propiedad MyOptions.Option1 se
enlaza a la clave Option1 , que se lee desde la propiedad option1 en appSettings.json.
En el código siguiente, se agrega un tercer servicio IConfigureOptions<TOptions> al contenedor de servicios.
Enlaza MySubOptions a la sección subsection del archivo appsettings.json:

// Example #3: Sub-options


// Bind options using a sub-section of the appsettings.json file.
services.Configure<MySubOptions>(Configuration.GetSection("subsection"));

El método de extensión GetSection requiere el paquete NuGet


Microsoft.Extensions.Options.ConfigurationExtensions. Si la aplicación usa el metapaquete
Microsoft.AspNetCore.All, el paquete se incluye automáticamente.
El archivo appSettings.json del ejemplo define un miembro subsection con las claves para suboption1 y
suboption2 :

{
"option1": "value1_from_json",
"option2": -1,
"subsection": {
"suboption1": "subvalue1_from_json",
"suboption2": 200
},
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*"
}

La clase MySubOptions define propiedades, SubOption1 y SubOption2 , para mantener los valores de
subopciones (Models/MySubOptions.cs):
public class MySubOptions
{
public MySubOptions()
{
// Set default values.
SubOption1 = "value1_from_ctor";
SubOption2 = 5;
}

public string SubOption1 { get; set; }


public int SubOption2 { get; set; }
}

El método OnGet del modelo de página devuelve una cadena con los valores de subopciones
(Pages/Index.cshtml.cs):

private readonly MySubOptions _subOptions;

public IndexModel(
IOptions<MyOptions> optionsAccessor,
IOptions<MyOptionsWithDelegateConfig> optionsAccessorWithDelegateConfig,
IOptions<MySubOptions> subOptionsAccessor,
IOptionsSnapshot<MyOptions> snapshotOptionsAccessor,
IOptionsSnapshot<MyOptions> namedOptionsAccessor)
{
_options = optionsAccessor.Value;
_optionsWithDelegateConfig = optionsAccessorWithDelegateConfig.Value;
_subOptions = subOptionsAccessor.Value;
_snapshotOptions = snapshotOptionsAccessor.Value;
_named_options_1 = namedOptionsAccessor.Get("named_options_1");
_named_options_2 = namedOptionsAccessor.Get("named_options_2");
}

// Example #3: Sub-options


var subOption1 = _subOptions.SubOption1;
var subOption2 = _subOptions.SubOption2;
SubOptions = $"subOption1 = {subOption1}, subOption2 = {subOption2}";

Cuando se ejecuta la aplicación, el método OnGet devuelve una cadena que muestra los valores de clase de
subopciones:

subOption1 = subvalue1_from_json, subOption2 = 200

Opciones proporcionadas por un modelo de vista o con inserción de


vista directa
Las opciones proporcionadas por un modelo de vista o de inserción de vista directa se muestran en el ejemplo
#4 en la aplicación de ejemplo.
Las opciones se pueden suministrar en un modelo de vista o insertando IOptions<TOptions> directamente en
una vista (Pages/Index.cshtml.cs):

private readonly MyOptions _options;


public IndexModel(
IOptions<MyOptions> optionsAccessor,
IOptions<MyOptionsWithDelegateConfig> optionsAccessorWithDelegateConfig,
IOptions<MySubOptions> subOptionsAccessor,
IOptionsSnapshot<MyOptions> snapshotOptionsAccessor,
IOptionsSnapshot<MyOptions> namedOptionsAccessor)
{
_options = optionsAccessor.Value;
_optionsWithDelegateConfig = optionsAccessorWithDelegateConfig.Value;
_subOptions = subOptionsAccessor.Value;
_snapshotOptions = snapshotOptionsAccessor.Value;
_named_options_1 = namedOptionsAccessor.Get("named_options_1");
_named_options_2 = namedOptionsAccessor.Get("named_options_2");
}

// Example #4: Bind options directly to the page


MyOptions = _options;

Para la inserción directa, inserte IOptions<MyOptions> con una directiva @inject :

@page
@model IndexModel
@using Microsoft.Extensions.Options
@using UsingOptionsSample.Models
@inject IOptions<MyOptions> OptionsAccessor
@{
ViewData["Title"] = "Using Options Sample";
}

<h1>@ViewData["Title"]</h1>

Cuando se ejecuta la aplicación, se muestran los valores de opciones en la página representada:

Volver a cargar los datos de configuración con IOptionsSnapshot


El procedimiento de volver a cargar los datos de configuración con IOptionsSnapshot se muestra en el ejemplo
#5 en la aplicación de ejemplo.
Requiere ASP.NET Core 1.1 o versiones posteriores.
IOptionsSnapshot admite volver a cargar opciones con la mínima sobrecarga de procesamiento. En ASP.NET
Core 1.1, IOptionsSnapshot es una instantánea de IOptionsMonitor<TOptions> y se actualiza
automáticamente cada vez que el monitor desencadena cambios de acuerdo con los cambios del origen de
datos. En ASP.NET Core 2.0 y versiones posteriores, las opciones se calculan una vez por solicitud cuando se
accede a ellas y se almacenan en caché en el tiempo que dura la solicitud.
En el ejemplo siguiente se muestra cómo se crea un nuevo IOptionsSnapshot después de cambiar el archivo
appSettings.json (Pages/Index.cshtml.cs). Varias solicitudes al servidor devuelven valores constantes
proporcionados por el archivo appSettings.json hasta que se modifique el archivo y vuelva a cargarse la
configuración.

private readonly MyOptions _snapshotOptions;

public IndexModel(
IOptions<MyOptions> optionsAccessor,
IOptions<MyOptionsWithDelegateConfig> optionsAccessorWithDelegateConfig,
IOptions<MySubOptions> subOptionsAccessor,
IOptionsSnapshot<MyOptions> snapshotOptionsAccessor,
IOptionsSnapshot<MyOptions> namedOptionsAccessor)
{
_options = optionsAccessor.Value;
_optionsWithDelegateConfig = optionsAccessorWithDelegateConfig.Value;
_subOptions = subOptionsAccessor.Value;
_snapshotOptions = snapshotOptionsAccessor.Value;
_named_options_1 = namedOptionsAccessor.Get("named_options_1");
_named_options_2 = namedOptionsAccessor.Get("named_options_2");
}

// Example #5: Snapshot options


var snapshotOption1 = _snapshotOptions.Option1;
var snapshotOption2 = _snapshotOptions.Option2;
SnapshotOptions =
$"snapshot option1 = {snapshotOption1}, " +
$"snapshot option2 = {snapshotOption2}";

En la siguiente imagen se muestran los valores option1 y option2 iniciales cargados desde el archivo
appSettings.json:

snapshot option1 = value1_from_json, snapshot option2 = -1

Cambie los valores del archivo appSettings.json a value1_from_json UPDATED y 200 . Guarde el archivo
appSettings.json. Actualice el explorador para ver qué valores de opciones se han actualizado:

snapshot option1 = value1_from_json UPDATED, snapshot option2 = 200

Compatibilidad de opciones con nombre con


IConfigureNamedOptions
La compatibilidad de opciones con nombre con IConfigureNamedOptions se muestra en el ejemplo #6 de la
aplicación de ejemplo.
Requiere ASP.NET Core 2.0 o versiones posteriores.
La compatibilidad con las opciones con nombre permite a la aplicación distinguir entre las configuraciones de
opciones con nombre. En la aplicación de ejemplo, las opciones con nombre se declaran con
OptionsServiceCollectionExtensions.Configure<TOptions>(IServiceCollection, cadena, acción<TOptions>)
que, a su vez, llama al método de extensión ConfigureNamedOptions<TOptions>.Configure:
// Example #6: Named options (named_options_1)
// Register the ConfigurationBuilder instance which MyOptions binds against.
// Specify that the options loaded from configuration are named
// "named_options_1".
services.Configure<MyOptions>("named_options_1", Configuration);

// Example #6: Named options (named_options_2)


// Specify that the options loaded from the MyOptions class are named
// "named_options_2".
// Use a delegate to configure option values.
services.Configure<MyOptions>("named_options_2", myOptions =>
{
myOptions.Option1 = "named_options_2_value1_from_action";
});

La aplicación de ejemplo accede a las opciones con nombre con IOptionsSnapshot<TOptions>.Get


(Pages/Index.cshtml.cs):

private readonly MyOptions _named_options_1;


private readonly MyOptions _named_options_2;

public IndexModel(
IOptions<MyOptions> optionsAccessor,
IOptions<MyOptionsWithDelegateConfig> optionsAccessorWithDelegateConfig,
IOptions<MySubOptions> subOptionsAccessor,
IOptionsSnapshot<MyOptions> snapshotOptionsAccessor,
IOptionsSnapshot<MyOptions> namedOptionsAccessor)
{
_options = optionsAccessor.Value;
_optionsWithDelegateConfig = optionsAccessorWithDelegateConfig.Value;
_subOptions = subOptionsAccessor.Value;
_snapshotOptions = snapshotOptionsAccessor.Value;
_named_options_1 = namedOptionsAccessor.Get("named_options_1");
_named_options_2 = namedOptionsAccessor.Get("named_options_2");
}

// Example #6: Named options


var named_options_1 =
$"named_options_1: option1 = {_named_options_1.Option1}, " +
$"option2 = {_named_options_1.Option2}";
var named_options_2 =
$"named_options_2: option1 = {_named_options_2.Option1}, " +
$"option2 = {_named_options_2.Option2}";
NamedOptions = $"{named_options_1} {named_options_2}";

Al ejecutar la aplicación de ejemplo, se devuelven las opciones con nombre:

named_options_1: option1 = value1_from_json, option2 = -1


named_options_2: option1 = named_options_2_value1_from_action, option2 = 5

Se proporcionan valores de named_options_1 a partir de la configuración, que se cargan desde el archivo


appSettings.json. Los valores de named_options_2 los proporciona:
El delegado named_options_2 en ConfigureServices para Option1 .
El valor predeterminado para Option2 proporcionado por la clase MyOptions .

Configure todas las instancias de opciones con nombre con el método


OptionsServiceCollectionExtensions.ConfigureAll. El siguiente código configura Option1 para todas las
instancias de configuración con nombre con un valor común. Agregue manualmente este código al método
Configure :

services.ConfigureAll<MyOptions>(myOptions =>
{
myOptions.Option1 = "ConfigureAll replacement value";
});

Al ejecutar la aplicación de ejemplo después de agregar el código se produce el siguiente resultado:

named_options_1: option1 = ConfigureAll replacement value, option2 = -1


named_options_2: option1 = ConfigureAll replacement value, option2 = 5

NOTE
En ASP.NET 2.0 Core y versiones posteriores, todas las opciones son instancias con nombre. Las instancias de
IConfigureOption existentes se usan para seleccionar como destino la instancia de Options.DefaultName , que es
string.Empty . IConfigureNamedOptions también implementa IConfigureOptions . La implementación
predeterminada de IOptionsFactory<TOptions> (origen de referencia tiene lógica para usar cada una de forma
adecuada. La opción con nombre null se usa para seleccionar como destino todas las instancias con nombre, en lugar
de una instancia con nombre determinada (ConfigureAll y PostConfigureAll usan esta convención).

IPostConfigureOptions
Requiere ASP.NET Core 2.0 o versiones posteriores.
Establezca la postconfiguración con IPostConfigureOptions<TOptions>. La postconfiguración se ejecuta
después de que tenga lugar toda la configuración de IConfigureOptions<TOptions>:

services.PostConfigure<MyOptions>(myOptions =>
{
myOptions.Option1 = "post_configured_option1_value";
});

PostConfigure<TOptions> está disponible para postconfigurar las opciones con nombre:

services.PostConfigure<MyOptions>("named_options_1", myOptions =>


{
myOptions.Option1 = "post_configured_option1_value";
});

Use PostConfigureAll<TOptions> para postconfigurar todas las instancias de configuración con nombre:

services.PostConfigureAll<MyOptions>("named_options_1", myOptions =>


{
myOptions.Option1 = "post_configured_option1_value";
});

Generador de opciones, supervisión y memoria caché


IOptionsMonitor se usa para crear notificaciones cuando cambian las instancias de TOptions .
IOptionsMonitor admite opciones recargables, notificaciones de cambios y IPostConfigureOptions .
IOptionsFactory<TOptions> (ASP.NET Core 2.0 o versiones posteriores) es responsable de crear nuevas
instancias de opciones. Tiene un solo método Create. La implementación predeterminada toma todas las
instancias registradas de IConfigureOptions y IPostConfigureOptions , y configura todas las configuraciones
primero, seguidas de las postconfiguraciones. Distingue entre IConfigureNamedOptions y IConfigureOptions , y
solo llama a la interfaz adecuada.
IOptionsMonitorCache<TOptions> (ASP.NET Core 2.0 o versiones posteriores) lo usa IOptionsMonitor para
almacenar en caché instancias de TOptions . IOptionsMonitorCache invalida instancias de opciones en la
supervisión para que se pueda volver a calcular el valor (TryRemove). Los valores se pueden introducir
manualmente y mediante TryAdd. Se usa el método Clear cuando todas las instancias con nombre se deben
volver a crear a petición.

Acceso a opciones durante el inicio


IOptions puede usarse en Configure , ya que los servicios se compilan antes de que se ejecute el método
Configure . Si se compila un proveedor de servicios en ConfigureServices para acceder a opciones, no incluirá
ninguna configuración de opción proporcionada después de que se compile el proveedor de servicios. Por
tanto, puede que exista un estado incoherente de opciones debido al orden de los registros de servicio.
Puesto que las opciones suelen cargarse a partir de la configuración, puede usar la configuración al inicio en
Configure y ConfigureServices . Para obtener ejemplos de uso de la configuración durante el inicio, vea el
tema Inicio de la aplicación ASP.NET Core.

Vea también
Configuración
Mejorar una aplicación desde un ensamblado
externo en ASP.NET Core con IHostingStartup
04/06/2018 • 10 minutes to read • Edit Online

Por Luke Latham


Una implementación de IHostingStartup permite agregar mejoras a una aplicación al iniciarla a partir de un
ensamblado externo fuera de la clase Startup de esta. Por ejemplo, una biblioteca de herramientas externas
puede usar una implementación de IHostingStartup para suministrar más servicios o proveedores de
configuración a una aplicación. IHostingStartup está disponible en ASP.NET 2.0 Core y versiones posteriores.
Vea o descargue el código de ejemplo (cómo descargarlo)

Detectar los ensamblados de inicio de hospedaje cargados


Para detectar los ensamblados de inicio de hospedaje que la aplicación o las bibliotecas han cargado, habilite el
registro y consulte los registros de la aplicación. En ellos se registran los errores que se producen al cargar
ensamblados. Los ensamblados de inicio de hospedaje se registran en el nivel de depuración y se registran
todos los errores.
La aplicación de ejemplo lee el valor de HostingStartupAssembliesKey en una matriz string y muestra el
resultado en la página de índice de la aplicación:

public class IndexModel : PageModel


{
private readonly IConfiguration _config;

public IndexModel(IConfiguration config)


{
_config = config;
}

public string[] LoadedHostingStartupAssemblies { get; private set; }

public void OnGet()


{
LoadedHostingStartupAssemblies =
_config[WebHostDefaults.HostingStartupAssembliesKey]
.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries) ?? new string[0];
}
}

Deshabilitar la carga automática de ensamblados de inicio de


hospedaje
Existen dos formas de deshabilitar la carga automática de ensamblados de inicio de hospedaje:
Establecer la opción de configuración de host para impedir el inicio de hospedaje.
Establecer la variable de entorno ASPNETCORE_PREVENTHOSTINGSTARTUP .
Cuando la configuración del host o la variable de entorno está establecida en true o 1 , los ensamblados de
inicio de hospedaje no se cargan automáticamente. Si ambas se han definido, será la configuración del host la
que controle el comportamiento.
Deshabilitar los ensamblados de inicio de hospedaje a través de la variable de entorno o de la configuración de
host hace que estos se deshabiliten globalmente y, asimismo, puede deshabilitar también otras características de
una aplicación. Actualmente no se puede deshabilitar de manera selectiva un ensamblado de inicio de hospedaje
que esté agregado a una biblioteca, a menos que esa biblioteca disponga de su propia opción de configuración.
En una próxima versión existirá la posibilidad de deshabilitar ensamblados de inicio de hospedaje de forma
selectiva (vea el problema de GitHub aspnet/Hosting n.º 1243).

Implementar IHostingStartup
Crear el ensamblado
Una mejora de IHostingStartup se implementa como un ensamblado basado en una aplicación de consola sin
un punto de entrada. El ensamblado hace referencia al paquete Microsoft.AspNetCore.Hosting.Abstractions:

<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions"
Version="2.1.0" />
</ItemGroup>

</Project>

Un atributo HostingStartup identifica una clase como una implementación de IHostingStartup para la carga y
ejecución al compilar IWebHost. En el siguiente ejemplo, el espacio de nombres es StartupEnhancement y la
clase, StartupEnhancementHostingStartup :

[assembly: HostingStartup(typeof(StartupEnhancement.StartupEnhancementHostingStartup))]

Una clase implementa IHostingStartup . El método Configurar de la clase usa un IWebHostBuilder para agregar
mejoras a una aplicación:

namespace StartupEnhancement
{
public class StartupEnhancementHostingStartup : IHostingStartup
{
public void Configure(IWebHostBuilder builder)
{
// Use the IWebHostBuilder to add app enhancements.
}
}
}

Al crear un proyecto de IHostingStartup , el archivo de dependencias (*.deps.json) establece la ubicación de


runtime del ensamblado en la carpeta bin:
"targets": {
".NETCoreApp,Version=v2.1": {
"StartupEnhancement/1.0.0": {
"dependencies": {
"Microsoft.AspNetCore.Hosting.Abstractions": "2.1.0"
},
"runtime": {
"StartupEnhancement.dll": {}
}
}
}
}

Solo se muestra parte del archivo. El nombre del ensamblado en el ejemplo es StartupEnhancement .
Actualizar el archivo de dependencias
La ubicación del tiempo de ejecución se especifica en el archivo *.deps.json. Para activar la mejora, el elemento
runtime debe especificar la ubicación del ensamblado de tiempo de ejecución de la mejora. Anteponga
lib/<TARGET_FRAMEWORK_MONIKER>/ a la ubicación de runtime :

"targets": {
".NETCoreApp,Version=v2.1": {
"StartupEnhancement/1.0.0": {
"dependencies": {
"Microsoft.AspNetCore.Hosting.Abstractions": "2.1.0"
},
"runtime": {
"lib/netcoreapp2.1/StartupEnhancement.dll": {}
}
}
}
}

En la aplicación de ejemplo, el archivo *.deps.json se modifica por medio de un script de PowerShell que se
activa automáticamente a través de un destino de compilación en el archivo de proyecto.
Activación de la mejora
Colocar el archivo de ensamblado
El archivo de ensamblado de la implementación IHostingStartup debe estar implementado en bin en la
aplicación o colocarse en el almacenamiento en tiempo de ejecución:
Para usarlo individualmente con cada usuario, coloque el ensamblado en el almacenamiento en tiempo de
ejecución del perfil del usuario en cuestión, en:

<DRIVE>\Users\<USER>\.dotnet\store\x64\<TARGET_FRAMEWORK_MONIKER>\<ENHANCEMENT_ASSEMBLY_NAME>\
<ENHANCEMENT_VERSION>\lib\<TARGET_FRAMEWORK_MONIKER>\

Para usarlo de manera global, colóquelo en el almacenamiento en tiempo de ejecución de la instalación de .NET
Core:

<DRIVE>\Program Files\dotnet\store\x64\<TARGET_FRAMEWORK_MONIKER>\<ENHANCEMENT_ASSEMBLY_NAME>\
<ENHANCEMENT_VERSION>\lib\<TARGET_FRAMEWORK_MONIKER>\

Si el ensamblado se implementa en el almacenamiento en tiempo de ejecución, puede que también se


implemente el archivo de símbolos, si bien esto no es imprescindible para que la mejora funcione.
Colocar el archivo de dependencias
El archivo *.deps.json de la implementación debe estar en una ubicación accesible.
Para usarlo individualmente con cada usuario, coloque el archivo en la carpeta additonalDeps de la
configuración .dotnet del perfil de usuario:

<DRIVE>\Users\<USER>\.dotnet\x64\additionalDeps\
<ENHANCEMENT_ASSEMBLY_NAME>\shared\Microsoft.NETCore.App\2.1.0\

Para usarlo de manera global, colóquelo en la carpeta additonalDeps de la instalación de .NET Core:

<DRIVE>\Program Files\dotnet\additionalDeps\<ENHANCEMENT_ASSEMBLY_NAME>\shared\Microsoft.NETCore.App\2.1.0\

La versión 2.1.0 es la versión del tiempo de ejecución compartido que la aplicación de destino usa. El tiempo
de ejecución compartido se muestra en el archivo *.runtimeconfig.json. En la aplicación de ejemplo, el tiempo de
ejecución compartido se especifica en el archivo HostingStartupSample.runtimeconfig.json.
Establecer las variables de entorno
Establezca las siguientes variables de entorno en el contexto de la aplicación que usa la mejora.
ASPNETCORE_HOSTINGSTARTUPASSEMBLIES
Solo se examinan los ensamblados de inicio de hospedaje en busca de HostingStartupAttribute . El nombre del
ensamblado de la implementación se proporciona en esta variable de entorno. En la aplicación de ejemplo, este
valor se establece en StartupDiagnostics .
Dicho valor también se puede establecer usando la configuración de host de ensamblados de inicio de
hospedaje.
DOTNET_ADDITIONAL_DEPS
Esta es la ubicación del archivo *.deps.json de la implementación.
Si el archivo está en la carpeta .dotnet del perfil de usuario para usarlo individualmente con cada usuario:

<DRIVE>\Users\<USER>\.dotnet\x64\additionalDeps\

Si el archivo está en la instalación de .NET Core para usarlo de manera global, indique la ruta de acceso
completa al archivo:

<DRIVE>\Program Files\dotnet\additionalDeps\<ENHANCEMENT_ASSEMBLY_NAME>\shared\Microsoft.NETCore.App\2.1.0\
<ENHANCEMENT_ASSEMBLY_NAME>.deps.json

En la aplicación de ejemplo, este valor se establece en:

%UserProfile%\.dotnet\x64\additionalDeps\StartupDiagnostics\

Para obtener ejemplos de cómo establecer variables de entorno en distintos sistemas operativos, vea Uso de
varios entornos.

Aplicación de ejemplo
La aplicación de ejemplo (cómo descargar) usa IHostingStartup para crear una herramienta de diagnóstico. Esta
herramienta agrega dos middlewares a la aplicación (mientras se inicia) que proporcionan información de
diagnóstico:
Servicios registrados
Dirección: esquema, host, ruta de acceso base, ruta de acceso, cadena de consulta
Conexión: dirección IP remota, puerto remoto, dirección IP local, puerto local, certificado de cliente
Encabezados de solicitud
Variables de entorno
Para ejecutar el ejemplo:
1. El proyecto de diagnóstico de inicio usa PowerShell para modificar el archivo StartupDiagnostics.deps.json.
PowerShell se instala de forma predeterminada en cualquier sistema operativo Windows a partir de
Windows 7 SP1 y Windows Server 2008 R2 SP1. Para obtener PowerShell en otras plataformas, vea
Instalación de Windows PowerShell.
2. Compile el proyecto de diagnóstico de inicio. Un destino de compilación en el archivo de proyecto hace lo
siguiente:
Mueve el ensamblado y los archivos de símbolos al almacenamiento en tiempo de ejecución del perfil
de usuario.
Activa el script de PowerShell para modificar el archivo StartupDiagnostics.deps.json.
Mueve el archivo StartupDiagnostics.deps.json a la carpeta additionalDeps del perfil de usuario.
3. Establezca las variables de entorno:
ASPNETCORE_HOSTINGSTARTUPASSEMBLIES : StartupDiagnostics
DOTNET_ADDITIONAL_DEPS : %UserProfile%\.dotnet\x64\additionalDeps\StartupDiagnostics\
4. Ejecute la aplicación de ejemplo.
5. Solicite al punto de conexión /services ver los servicios registrados de la aplicación. Solicite al punto de
conexión /diag ver la información de diagnóstico.
Registro en ASP.NET Core
21/06/2018 • 42 minutes to read • Edit Online

Por Steve Smith y Tom Dykstra


ASP.NET Core es compatible con una API de registro que funciona con una variedad de proveedores de
registro. Los proveedores integrados permiten enviar registros a uno o varios destinos, y se puede conectar
una plataforma de registro de terceros. En este artículo se muestra cómo usar las API y los proveedores de
registro integrados en el código.
ASP.NET Core 2.x
ASP.NET Core 1.x
Vea o descargue el código de ejemplo (cómo descargarlo)

Cómo crear registros


Para crear registros, implemente un objeto ILogger desde el contenedor de inserción de dependencias:

public class TodoController : Controller


{
private readonly ITodoRepository _todoRepository;
private readonly ILogger _logger;

public TodoController(ITodoRepository todoRepository,


ILogger<TodoController> logger)
{
_todoRepository = todoRepository;
_logger = logger;
}

Después, llame a los métodos de registro de ese objeto de registrador:

public IActionResult GetById(string id)


{
_logger.LogInformation(LoggingEvents.GetItem, "Getting item {ID}", id);
var item = _todoRepository.Find(id);
if (item == null)
{
_logger.LogWarning(LoggingEvents.GetItemNotFound, "GetById({ID}) NOT FOUND", id);
return NotFound();
}
return new ObjectResult(item);
}

En este ejemplo se crean registros con la clase TodoController como categoría. Las categorías se explican
más adelante en este artículo.
ASP.NET Core no proporciona métodos de registrador asincrónicos porque el registro debe ser tan rápido
que el costo de usarlos no vale la pena. Si se encuentra en una situación en la que no sea así, considere la
posibilidad de cambiar el modo de registro. Si el almacén de datos es lento, escriba primero los mensajes de
registro en un almacén rápido y, después, muévalos a un almacén de baja velocidad. Por ejemplo, realice el
registro en una cola de mensajes que otro proceso lea y conserve en almacenamiento lento.
Cómo agregar proveedores
ASP.NET Core 2.x
ASP.NET Core 1.x
Un proveedor de registro toma los mensajes que se crean con un objeto ILogger y los muestra o almacena.
Por ejemplo, el proveedor de la consola muestra mensajes en la consola y el proveedor de Azure App Service
puede almacenarlos en Azure Blob Storage.
Para usar un proveedor, llame al método de extensión Add<ProviderName> del proveedor en Program.cs:

public static void Main(string[] args)


{
var webHost = new WebHostBuilder()
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.ConfigureAppConfiguration((hostingContext, config) =>
{
var env = hostingContext.HostingEnvironment;
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange:
true);
config.AddEnvironmentVariables();
})
.ConfigureLogging((hostingContext, logging) =>
{
logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
logging.AddConsole();
logging.AddDebug();
})
.UseStartup<Startup>()
.Build();

webHost.Run();
}

La plantilla de proyecto predeterminada permite el registro con el método CreateDefaultBuilder:

public static void Main(string[] args)


{
BuildWebHost(args).Run();
}

public static IWebHost BuildWebHost(string[] args) =>


WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.Build();

Puede encontrar información sobre cada proveedor de registro integrado y vínculos a proveedores de
registro de terceros más adelante en el artículo.

Salida de registro de ejemplo


Con el código de ejemplo que se muestra en la sección anterior, verá los registros en la consola cuando se
ejecute desde la línea de comandos. Este es un ejemplo de salida de la consola:
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
Request starting HTTP/1.1 GET http://localhost:5000/api/todo/0
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[1]
Executing action method TodoApi.Controllers.TodoController.GetById (TodoApi) with arguments (0) -
ModelState is Valid
info: TodoApi.Controllers.TodoController[1002]
Getting item 0
warn: TodoApi.Controllers.TodoController[4000]
GetById(0) NOT FOUND
info: Microsoft.AspNetCore.Mvc.StatusCodeResult[1]
Executing HttpStatusCodeResult, setting HTTP status code 404
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2]
Executed action TodoApi.Controllers.TodoController.GetById (TodoApi) in 42.9286ms
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
Request finished in 148.889ms 404

Estos registros se crearon yendo a http://localhost:5000/api/todo/0 , lo que desencadena la ejecución de las


dos llamadas a ILogger que se muestran en la sección anterior.
Este es un ejemplo de los mismos registros tal y como aparecen en la ventana de depuración cuando se
ejecuta la aplicación de ejemplo en Visual Studio:

Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request starting HTTP/1.1 GET


http://localhost:53104/api/todo/0
Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker:Information: Executing action method
TodoApi.Controllers.TodoController.GetById (TodoApi) with arguments (0) - ModelState is Valid
TodoApi.Controllers.TodoController:Information: Getting item 0
TodoApi.Controllers.TodoController:Warning: GetById(0) NOT FOUND
Microsoft.AspNetCore.Mvc.StatusCodeResult:Information: Executing HttpStatusCodeResult, setting HTTP status
code 404
Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker:Information: Executed action
TodoApi.Controllers.TodoController.GetById (TodoApi) in 152.5657ms
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request finished in 316.3195ms 404

Los registros creados por las llamadas a ILogger que se muestran en la sección anterior comienzan por
"TodoApi.Controllers.TodoController". Los registros que comienzan por categorías de "Microsoft" son de
ASP.NET Core. El propio ASP.NET Core y el código de la aplicación usan la misma API y los mismos
proveedores de registro.
En el resto de este artículo se explican algunos detalles y opciones para el registro.

Paquetes NuGet
Las interfaces ILogger e ILoggerFactory se encuentran en Microsoft.Extensions.Logging.Abstractions, y sus
implementaciones predeterminadas en Microsoft.Extensions.Logging.

Categoría de registro
Con cada registro que cree se incluye una categoría. La categoría se especifica cuando se crea un objeto
ILogger . La categoría puede ser cualquier cadena, pero una convención consiste en usar el nombre completo
de la clase desde la que se escriben los registros. Por ejemplo: "TodoApi.Controllers.TodoController".
Puede especificar la categoría como una cadena o usar un método de extensión que derive la categoría del
tipo. Para especificar la categoría como una cadena, llame a CreateLogger en una instancia de ILoggerFactory
, como se muestra a continuación.
public class TodoController : Controller
{
private readonly ITodoRepository _todoRepository;
private readonly ILogger _logger;

public TodoController(ITodoRepository todoRepository,


ILoggerFactory logger)
{
_todoRepository = todoRepository;
_logger = logger.CreateLogger("TodoApi.Controllers.TodoController");
}

En la mayoría de los casos, será más fácil usar ILogger<T> , como en el ejemplo siguiente.

public class TodoController : Controller


{
private readonly ITodoRepository _todoRepository;
private readonly ILogger _logger;

public TodoController(ITodoRepository todoRepository,


ILogger<TodoController> logger)
{
_todoRepository = todoRepository;
_logger = logger;
}

Esto equivale a llamar a CreateLogger con el nombre de tipo completo de T .

Nivel de registro
Cada vez que se escribe un registro, se especifica su LogLevel. El nivel de registro indica el grado de gravedad
o importancia. Por ejemplo, es posible que escriba un registro Information cuando un método finaliza
normalmente, un registro Warning cuando un método devuelve un código de retorno 404 y un registro
Error cuando capture una excepción inesperada.

En el ejemplo de código siguiente, los nombres de los métodos (por ejemplo, LogWarning ) especifican el nivel
de registro. El primer parámetro es el Id. del evento del Registro. El segundo parámetro es una plantilla de
mensaje con marcadores de posición para los valores de argumento proporcionados por el resto de
parámetros de método. Los parámetros de método se explican detalladamente más adelante en este artículo.

public IActionResult GetById(string id)


{
_logger.LogInformation(LoggingEvents.GetItem, "Getting item {ID}", id);
var item = _todoRepository.Find(id);
if (item == null)
{
_logger.LogWarning(LoggingEvents.GetItemNotFound, "GetById({ID}) NOT FOUND", id);
return NotFound();
}
return new ObjectResult(item);
}

Los métodos de registro que incluyen el nivel en el nombre del método son métodos de extensión para
ILogger. En segundo plano, estos métodos llaman a un método Log que toma un parámetro LogLevel .
Puede llamar directamente al método Log en lugar de a uno de estos métodos de extensión, pero la sintaxis
es relativamente complicada. Para más información, vea la interfaz ILogger y el código fuente de las
extensiones de registrador.
ASP.NET Core define los niveles de registro siguientes, que aquí se ordenan de menor a mayor gravedad.
Seguimiento = 0
Para la información que solo es útil para un desarrollador que depura un problema. Estos mensajes
pueden contener datos confidenciales de la aplicación, por lo que no deben habilitarse en un entorno
de producción. Deshabilitado de forma predeterminada. Ejemplo:
Credentials: {"User":"someuser", "Password":"P@ssword"}

Depurar = 1
Para la información que tiene utilidad a corto plazo durante el desarrollo y la depuración. Ejemplo:
Entering method Configure with flag set to true. normalmente los registros de nivel Debug no se
habilitarían en producción, a menos que se esté solucionando un problema, debido al elevado volumen
de registros.
Información = 2
Para realizar el seguimiento del flujo general de la aplicación. Estos registros suelen tener algún valor a
largo plazo. Ejemplo: Request received for path /api/todo
Advertencia = 3
Para los eventos anómalos o inesperados en el flujo de la aplicación. Estos pueden incluir errores u
otras condiciones que no hacen que la aplicación se detenga, pero que puede que sea necesario
investigar. Las excepciones controladas son un lugar común para usar el nivel de registro Warning .
Ejemplo: FileNotFoundException for file quotes.txt.
Error = 4
Para los errores y excepciones que no se pueden controlar. Estos mensajes indican un error en la
actividad u operación actual (por ejemplo, la solicitud HTTP actual), no un error de toda la aplicación.
Mensaje de registro de ejemplo: Cannot insert record due to duplicate key violation.
Crítico = 5
Para los errores que requieren atención inmediata. Ejemplos: escenarios de pérdida de datos, espacio
en disco insuficiente.
Puede usar el nivel de registro para controlar la cantidad de salida del registro que se escribe en un medio de
almacenamiento determinado o ventana de presentación. Por ejemplo, en producción es posible que quiera
que todos los registros de nivel Information e inferiores vayan a un almacén de datos de volumen y que
todos los registros de nivel Warning y superiores vayan un almacén de datos de valor. Durante el desarrollo,
es posible que normalmente envíe los registros de gravedad Warning o superior a la consola. Después, si
tiene que solucionar problemas, puede agregar el nivel Debug . En la sección Filtrado del registro de este
artículo se explica cómo controlar los niveles de registro que controla un proveedor.
La plataforma ASP.NET Core escribe registros de nivel Debug para los eventos de la plataforma. En los
ejemplos de registro anteriores de este artículo se excluyeron los registros por debajo del nivel Information ,
por lo que no se mostraron los registros de nivel Debug . Este es un ejemplo de registros de consola si ejecuta
la aplicación de ejemplo configurada para mostrar Debug y registros superiores para el proveedor de la
consola.
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
Request starting HTTP/1.1 GET http://localhost:62555/api/todo/0
dbug: Microsoft.AspNetCore.Routing.Tree.TreeRouter[1]
Request successfully matched the route with name 'GetTodo' and template 'api/Todo/{id}'.
dbug: Microsoft.AspNetCore.Mvc.Internal.ActionSelector[2]
Action 'TodoApi.Controllers.TodoController.Update (TodoApi)' with id '089d59b6-92ec-472d-b552-
cc613dfd625d' did not match the constraint 'Microsoft.AspNetCore.Mvc.Internal.HttpMethodActionConstraint'
dbug: Microsoft.AspNetCore.Mvc.Internal.ActionSelector[2]
Action 'TodoApi.Controllers.TodoController.Delete (TodoApi)' with id 'f3476abe-4bd9-4ad3-9261-
3ead09607366' did not match the constraint 'Microsoft.AspNetCore.Mvc.Internal.HttpMethodActionConstraint'
dbug: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[1]
Executing action TodoApi.Controllers.TodoController.GetById (TodoApi)
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[1]
Executing action method TodoApi.Controllers.TodoController.GetById (TodoApi) with arguments (0) -
ModelState is Valid
info: TodoApi.Controllers.TodoController[1002]
Getting item 0
warn: TodoApi.Controllers.TodoController[4000]
GetById(0) NOT FOUND
dbug: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2]
Executed action method TodoApi.Controllers.TodoController.GetById (TodoApi), returned result
Microsoft.AspNetCore.Mvc.NotFoundResult.
info: Microsoft.AspNetCore.Mvc.StatusCodeResult[1]
Executing HttpStatusCodeResult, setting HTTP status code 404
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2]
Executed action TodoApi.Controllers.TodoController.GetById (TodoApi) in 0.8788ms
dbug: Microsoft.AspNetCore.Server.Kestrel[9]
Connection id "0HL6L7NEFF2QD" completed keep alive response.
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
Request finished in 2.7286ms 404

Id. de evento del registro


Cada vez que se escribe un registro, puede especificar un Id. de evento. La aplicación de ejemplo lo hace
mediante una clase LoggingEvents definida de forma local:

public IActionResult GetById(string id)


{
_logger.LogInformation(LoggingEvents.GetItem, "Getting item {ID}", id);
var item = _todoRepository.Find(id);
if (item == null)
{
_logger.LogWarning(LoggingEvents.GetItemNotFound, "GetById({ID}) NOT FOUND", id);
return NotFound();
}
return new ObjectResult(item);
}

public class LoggingEvents


{
public const int GenerateItems = 1000;
public const int ListItems = 1001;
public const int GetItem = 1002;
public const int InsertItem = 1003;
public const int UpdateItem = 1004;
public const int DeleteItem = 1005;

public const int GetItemNotFound = 4000;


public const int UpdateItemNotFound = 4001;
}
Un identificador de evento es un valor entero que se puede usar para asociar un conjunto de eventos
registrados entre sí. Por ejemplo, un registro para agregar un elemento a un carro de la compra podría tener
el identificador de evento 1000 y un registro para completar una compra podría tener el identificador de
evento 1001.
En la salida del registro, el identificador de evento podría estar almacenado en un campo o incluido en el
mensaje de texto, en función del proveedor. El proveedor de depuración no muestra los identificadores de
evento, pero el proveedor de la consola los muestra entre paréntesis después de la categoría:

info: TodoApi.Controllers.TodoController[1002]
Getting item invalidid
warn: TodoApi.Controllers.TodoController[4000]
GetById(invalidid) NOT FOUND

Plantilla de mensaje de registro


Cada vez que se escribe un mensaje de registro, se proporciona una plantilla de mensaje. La plantilla de
mensaje puede ser una cadena o puede contener marcadores de posición con nombre en los que se colocan
los valores de argumento. La plantilla no es una cadena de formato y los marcadores de posición deben tener
un nombre, no un número.

public IActionResult GetById(string id)


{
_logger.LogInformation(LoggingEvents.GetItem, "Getting item {ID}", id);
var item = _todoRepository.Find(id);
if (item == null)
{
_logger.LogWarning(LoggingEvents.GetItemNotFound, "GetById({ID}) NOT FOUND", id);
return NotFound();
}
return new ObjectResult(item);
}

El orden de los marcadores de posición, no sus nombres, determina qué parámetros se usan para
proporcionar sus valores. Si tiene el siguiente código:

string p1 = "parm1";
string p2 = "parm2";
_logger.LogInformation("Parameter values: {p2}, {p1}", p1, p2);

El mensaje del registro resultante tiene el aspecto siguiente:

Parameter values: parm1, parm2

La plataforma de registro aplica este formato de mensajes para que los proveedores de registro puedan
implementar el registro semántico, también conocido como registro estructurado. Dado que los propios
argumentos se pasan al sistema de registro, no solo la plantilla de mensaje con formato, los proveedores de
registro pueden almacenar los valores de parámetros como campos además de la plantilla de mensaje. Si va a
dirigir la salida del registro a Azure Table Storage y la llamada al método de registrador tiene el aspecto
siguiente:

_logger.LogInformation("Getting item {ID} at {RequestTime}", id, DateTime.Now);

Cada entidad de la Tabla de Azure puede tener propiedades ID y RequestTime , lo que simplifica las consultas
en los datos de registro. Puede buscar todos los registros dentro de un intervalo RequestTime determinado
sin necesidad de analizar el tiempo de espera del mensaje de texto.

Excepciones de registro
Los métodos de registrador tienen sobrecargas que le permiten pasar una excepción, como en el ejemplo
siguiente:

catch (Exception ex)


{
_logger.LogWarning(LoggingEvents.GetItemNotFound, ex, "GetById({ID}) NOT FOUND", id);
return NotFound();
}
return new ObjectResult(item);

Cada proveedor controla la información de la excepción de maneras diferentes. Este es un ejemplo de salida
del proveedor de depuración del código mostrado anteriormente.

TodoApi.Controllers.TodoController:Warning: GetById(036dd898-fb01-47e8-9a65-f92eb73cf924) NOT FOUND

System.Exception: Item not found exception.


at TodoApi.Controllers.TodoController.GetById(String id) in
C:\logging\sample\src\TodoApi\Controllers\TodoController.cs:line 226

Filtrado del registro


ASP.NET Core 2.x
ASP.NET Core 1.x
Puede especificar un nivel de registro mínimo para un proveedor y una categoría específicos, o para todos los
proveedores o todas las categorías. Los registros por debajo del nivel mínimo no se pasan a ese proveedor, de
modo que no se muestran o almacenan.
Si quiere suprimir todos los registros, puede especificar LogLevel.None como el nivel de registro mínimo. El
valor entero de LogLevel.None es 6, que es superior a LogLevel.Critical (5).
Crear reglas de filtro en la configuración
Las plantillas de proyecto crean código que llama a CreateDefaultBuilder para configurar el registro para los
proveedores de consola y de depuración. El método CreateDefaultBuilder también establece el registro para
buscar la configuración en una sección Logging , mediante código similar al siguiente:
public static void Main(string[] args)
{
var webHost = new WebHostBuilder()
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.ConfigureAppConfiguration((hostingContext, config) =>
{
var env = hostingContext.HostingEnvironment;
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange:
true);
config.AddEnvironmentVariables();
})
.ConfigureLogging((hostingContext, logging) =>
{
logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
logging.AddConsole();
logging.AddDebug();
})
.UseStartup<Startup>()
.Build();

webHost.Run();
}

Los datos de configuración especifican niveles de registro mínimo por proveedor y categoría, como en el
ejemplo siguiente:

{
"Logging": {
"Debug": {
"LogLevel": {
"Default": "Information"
}
},
"Console": {
"IncludeScopes": false,
"LogLevel": {
"Microsoft.AspNetCore.Mvc.Razor.Internal": "Warning",
"Microsoft.AspNetCore.Mvc.Razor.Razor": "Debug",
"Microsoft.AspNetCore.Mvc.Razor": "Error",
"Default": "Information"
}
},
"LogLevel": {
"Default": "Debug"
}
}
}

Este archivo JSON crea seis reglas de filtro, una para el proveedor de depuración, cuatro para el proveedor de
la consola y una que se aplica a todos los proveedores. Más adelante verá que solo una de estas reglas se
elige para cada proveedor cuando se crea un objeto ILogger .
Reglas de filtro en el código
Puede registrar las reglas de filtro en el código, como se muestra en el ejemplo siguiente:
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.ConfigureLogging(logging =>
logging.AddFilter("System", LogLevel.Debug)
.AddFilter<DebugLoggerProvider>("Microsoft", LogLevel.Trace))
.Build();

El segundo AddFilter especifica el proveedor de depuración mediante su nombre de tipo. El primer


AddFilter se aplica a todos los proveedores, dado que no especifica un tipo de proveedor.
Cómo se aplican las reglas de filtro
Los datos de configuración y el código de AddFilter que se muestran en los ejemplos anteriores crean las
reglas que se muestran en la tabla siguiente. Las seis primeras proceden del ejemplo de configuración y las
dos últimas del ejemplo de código.

CATEGORÍAS QUE
NÚMERO PROVEEDOR COMIENZAN POR... NIVEL DE REGISTRO MÍNIMO

1 Depuración Todas las categorías Información

2 Consola Microsoft.AspNetCore.Mvc. Advertencia


Razor.Internal

3 Consola Microsoft.AspNetCore.Mvc. Depuración


Razor.Razor

4 Consola Microsoft.AspNetCore.Mvc. Error


Razor

5 Consola Todas las categorías Información

6 Todos los proveedores Todas las categorías Depuración

7 Todos los proveedores Sistema Depuración

8 Depuración Microsoft Seguimiento

Cuando se crea un objeto ILogger con el que escribir los registros, el objeto ILoggerFactory selecciona una
sola regla por proveedor para aplicar a ese registrador. Todos los mensajes escritos por ese objeto ILogger se
filtran según las reglas seleccionadas. De las reglas disponibles se selecciona la más específica posible para
cada par de categoría y proveedor.
Cuando se crea un ILogger para una categoría determinada, se usa el algoritmo siguiente para cada
proveedor:
Se seleccionan todas las reglas que coinciden con el proveedor o su alias. Si no se encuentra ninguna, se
seleccionan todas las reglas con un proveedor vacío.
Del resultado del paso anterior, se seleccionan las reglas con el prefijo de categoría coincidente más largo.
Si no se encuentra ninguna, se seleccionan todas las reglas que no especifican una categoría.
Si se seleccionan varias reglas, se toma la última.
Si no se selecciona ninguna regla, se usa MinimumLevel .

Por ejemplo, supongamos que tiene la lista de reglas anterior y crea un objeto ILogger para la categoría
"Microsoft.AspNetCore.Mvc.Razor.RazorViewEngine":
Para el proveedor de depuración, se aplican las reglas 1, 6 y 8. La regla 8 es la más específica, por lo que se
selecciona.
Para el proveedor de la consola, se aplican las reglas 3, 4, 5 y 6. La regla 3 es la más específica.
Cuando crea registros con un ILogger para la categoría
"Microsoft.AspNetCore.Mvc.Razor.RazorViewEngine", los registros de nivel Trace y superiores se dirigirán al
proveedor de depuración y los registros de nivel Debug y superiores se dirigirán al proveedor de consola.
Alias de proveedor
Puede usar el nombre de tipo para especificar un proveedor en la configuración, pero cada proveedor define
un alias más breve que es más fácil de usar. Para los proveedores integrados, use los alias siguientes:
Consola
Depuración
EventLog
AzureAppServices
TraceSource
EventSource
Nivel mínimo predeterminado
Hay una configuración de nivel mínimo que solo tiene efecto si no se aplica ninguna regla de configuración o
código para un proveedor y una categoría determinados. En el ejemplo siguiente se muestra cómo establecer
el nivel mínimo:

WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.ConfigureLogging(logging => logging.SetMinimumLevel(LogLevel.Warning))
.Build();

Si no establece explícitamente el nivel mínimo, el valor predeterminado es Information , lo que significa que
los registros Trace y Debug se omiten.
Funciones de filtro
Puede escribir código en una función de filtro para aplicar las reglas de filtrado. Se invoca una función de filtro
para todos los proveedores y categorías que no tienen reglas asignadas mediante configuración o código. El
código de la función tiene acceso al tipo de proveedor, categoría y nivel de registro para decidir si se debe
registrar un mensaje. Por ejemplo:

WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.ConfigureLogging(logBuilder =>
{
logBuilder.AddFilter((provider, category, logLevel) =>
{
if (provider == "Microsoft.Extensions.Logging.Console.ConsoleLoggerProvider" &&
category == "TodoApi.Controllers.TodoController")
{
return false;
}
return true;
});
})
.Build();
Ámbitos de registro
Puede agrupar un conjunto de operaciones lógicas dentro de un ámbito para adjuntar los mismos datos para
cada registro que se crea como parte de ese conjunto. Por ejemplo, es posible que quiera que todos los
registros creados como parte del procesamiento de una transacción incluyan el identificador de la transacción.
Un ámbito es un tipo IDisposable devuelto por el método ILogger.BeginScope<TState> que se conserva
hasta que se elimina. Para usar un ámbito, las llamadas de registrador se encapsulan en un bloque using ,
como se muestra aquí:

public IActionResult GetById(string id)


{
TodoItem item;
using (_logger.BeginScope("Message attached to logs created in the using block"))
{
_logger.LogInformation(LoggingEvents.GetItem, "Getting item {ID}", id);
item = _todoRepository.Find(id);
if (item == null)
{
_logger.LogWarning(LoggingEvents.GetItemNotFound, "GetById({ID}) NOT FOUND", id);
return NotFound();
}
}
return new ObjectResult(item);
}

El código siguiente permite ámbitos para el proveedor de la consola:


ASP.NET Core 2.x
ASP.NET Core 1.x
En Program.cs:

.ConfigureLogging((hostingContext, logging) =>


{
logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
logging.AddConsole(options => options.IncludeScopes = true);
logging.AddDebug();
})

NOTE
Es necesario configurar la opción del registrador de consola IncludeScopes para habilitar el registro basado en el
ámbito. La configuración de IncludeScopes con archivos de configuración appsettings estará disponible con la
versión de ASP.NET Core 2.1.

Cada mensaje de registro incluye la información de ámbito:

info: TodoApi.Controllers.TodoController[1002]
=> RequestId:0HKV9C49II9CK RequestPath:/api/todo/0 => TodoApi.Controllers.TodoController.GetById
(TodoApi) => Message attached to logs created in the using block
Getting item 0
warn: TodoApi.Controllers.TodoController[4000]
=> RequestId:0HKV9C49II9CK RequestPath:/api/todo/0 => TodoApi.Controllers.TodoController.GetById
(TodoApi) => Message attached to logs created in the using block
GetById(0) NOT FOUND
Proveedores de registro integrados
ASP.NET Core incluye los proveedores siguientes:
Consola
Depurar
EventSource
EventLog
TraceSource
Azure App Service
Proveedor de la consola
El paquete de proveedor Microsoft.Extensions.Logging.Console envía la salida del registro a la consola.
ASP.NET Core 2.x
ASP.NET Core 1.x

logging.AddConsole()

Proveedor de depuración
El paquete de proveedor Microsoft.Extensions.Logging.Debug escribe la salida del registro mediante la clase
System.Diagnostics.Debug (llamadas a métodos Debug.WriteLine ).
En Linux, este proveedor escribe registros en /var/log/message.
ASP.NET Core 2.x
ASP.NET Core 1.x

logging.AddDebug()

Proveedor EventSource
Para las aplicaciones que tienen como destino ASP.NET Core 1.1.0 o superior, el paquete de proveedor
Microsoft.Extensions.Logging.EventSource puede implementar el seguimiento de eventos. En Windows, usa
ETW. Es un proveedor multiplataforma, pero todavía no hay herramientas de recopilación y visualización de
eventos para Linux o macOS.
ASP.NET Core 2.x
ASP.NET Core 1.x

logging.AddEventSourceLogger()

Una buena manera de recopilar y ver los registros es usar la utilidad PerfView. Hay otras herramientas para
ver los registros ETW, pero PerfView proporciona la mejor experiencia para trabajar con los eventos ETW
emitidos por ASP.NET.
Para configurar PerfView para la recopilación de eventos registrados por este proveedor, agregue la cadena
*Microsoft-Extensions-Logging a la lista Proveedores adicionales. ( No olvide el asterisco al principio de la
cadena).
Proveedor EventLog de Windows
El paquete de proveedor Microsoft.Extensions.Logging.EventLog envía la salida del registro al Registro de
eventos de Windows.
ASP.NET Core 2.x
ASP.NET Core 1.x

logging.AddEventLog()

Proveedor TraceSource
El paquete de proveedor Microsoft.Extensions.Logging.TraceSource usa las bibliotecas y proveedores de
System.Diagnostics.TraceSource.
ASP.NET Core 2.x
ASP.NET Core 1.x

logging.AddTraceSource(sourceSwitchName);

Las sobrecargas de AddTraceSource permiten pasar un modificador de origen y un agente de escucha de


seguimiento.
Para usar este proveedor, una aplicación debe ejecutarse en .NET Framework (en lugar de .NET Core). El
proveedor permite enrutar mensajes a una variedad de agentes de escucha, como TextWriterTraceListener
que se usa en la aplicación de ejemplo.
En el ejemplo siguiente se configura un proveedor TraceSource que registra mensajes Warning y superiores
en la ventana de consola.
public void Configure(IApplicationBuilder app,
IHostingEnvironment env,
ILoggerFactory loggerFactory)
{
loggerFactory
.AddDebug();

// add Trace Source logging


var testSwitch = new SourceSwitch("sourceSwitch", "Logging Sample");
testSwitch.Level = SourceLevels.Warning;
loggerFactory.AddTraceSource(testSwitch,
new TextWriterTraceListener(writer: Console.Out));

Proveedor Azure App Service


El paquete de proveedor Microsoft.Extensions.Logging.AzureAppServices escribe los registros en archivos de
texto en el sistema de archivos de una aplicación de Azure App Service y en Blob Storage en una cuenta de
Azure Storage. El proveedor solo está disponible para las aplicaciones que tienen como destino ASP.NET Core
1.1 o posterior.
ASP.NET Core 2.x
ASP.NET Core 1.x
Si el destino es .NET Core, no instale el paquete de proveedor ni llame explícitamente a
AddAzureWebAppDiagnostics. El proveedor está disponible automáticamente para la aplicación cuando esta
se implementa en Azure App Service.
Si el destino es .NET Framework, agregue el paquete de proveedor al proyecto e invoque a
AddAzureWebAppDiagnostics :

logging.AddAzureWebAppDiagnostics();

Cuando se realiza una implementación en una aplicación de App Service, la aplicación respeta la
configuración de la sección Registros de diagnóstico situada en la página App Service de Azure Portal.
Cuando se actualiza esta configuración, los cambios se aplican inmediatamente sin necesidad de reiniciar ni
de volver a implementar la aplicación.
La ubicación predeterminada de los archivos de registro es la carpeta D:\home\LogFiles\Application y el
nombre de archivo predeterminado es diagnostics-aaaammdd.txt. El límite de tamaño de archivo
predeterminado es 10 MB, y el número máximo predeterminado de archivos que se conservan es 2. El
nombre de blob predeterminado es {nombre-de-la -aplicación}{marca de tiempo }/aaaa/mm/dd/hh/{guid }-
applicationLog.txt. Para más información sobre el comportamiento predeterminado, vea
AzureAppServicesDiagnosticsSettings.
El proveedor solo funciona cuando el proyecto se ejecuta en el entorno de Azure. No tiene ningún efecto
cuando el proyecto se ejecuta de manera local (no escribe en los archivos locales ni en el almacenamiento de
desarrollo local de blobs).

Proveedores de registro de terceros


Plataformas de registro de terceros que funcionan con ASP.NET Core:
elmah.io (repositorio de GitHub)
Gelf (repositorio de GitHub)
JSNLog (repositorio de GitHub)
Loggr (repositorio de GitHub)
NLog (repositorio de GitHub)
Serilog (repositorio de GitHub)
Algunas plataformas de terceros pueden realizar registro semántico, también conocido como registro
estructurado.
El uso de una plataforma de terceros es similar al uso de uno de los proveedores integrados:
1. Agregue un paquete NuGet al proyecto.
2. Llame a un método de extensión en ILoggerFactory .

Para más información, vea la documentación de cada plataforma.

Secuencias de registro de Azure


Las secuencias de registro de Azure permiten ver la actividad de registro en tiempo real desde:
El servidor de aplicaciones
El servidor web
Error del seguimiento de solicitudes
Para configurar las secuencias de registro de Azure:
Navegue hasta la página Registros de diagnóstico desde la página de portal de la aplicación
Establezca Registro de la aplicación (sistema de archivos) en Activado.

Navegue hasta la página Secuencias de registro para ver los mensajes de la aplicación. Se registran por la
aplicación a través de la interfaz ILogger .
Recursos adicionales
Registro de alto rendimiento con LoggerMessage
Registro de alto rendimiento con LoggerMessage en
ASP.NET Core
14/05/2018 • 13 minutes to read • Edit Online

Por Luke Latham


Las características de LoggerMessage crean delegados almacenables en caché que requieren menos asignaciones
de objetos y una menor sobrecarga computacional que los métodos de extensión del registrador, como
LogInformation , LogDebug y LogError . Para escenarios de registro de alto rendimiento, use el patrón
LoggerMessage .

LoggerMessage proporciona las siguientes ventajas de rendimiento frente a los métodos de extensión del
registrador:
Los métodos de extensión del registrador requieren la conversión boxing de tipos de valor, como int , en
object . El patrón LoggerMessage impide la conversión boxing mediante métodos de extensión y campos
Action estáticos con parámetros fuertemente tipados.
Los métodos de extensión del registrador deben analizar la plantilla de mensaje (cadena de formato con
nombre) cada vez que se escribe un mensaje de registro. LoggerMessage solo necesita analizar una vez una
plantilla cuando se define el mensaje.
Vea o descargue el código de ejemplo (cómo descargarlo)
La aplicación de ejemplo muestra las características de LoggerMessage con un sistema de seguimiento de citas
básico. La aplicación agrega y elimina citas mediante una base de datos en memoria. A medida que se producen
estas operaciones, se generan mensajes de registro mediante el patrón LoggerMessage .

LoggerMessage.Define
Define(LogLevel, EventId, String) crea un delegado Action para registrar un mensaje. Las sobrecargas Define
permiten pasar hasta seis parámetros de tipo a una cadena de formato con nombre (plantilla).
La cadena proporcionada al método Define es una plantilla y no una cadena interpolada. Los marcadores de
posición se rellenan en el orden en que se especifican los tipos. Los nombres de los marcadores de posición en la
plantilla deben ser descriptivos y coherentes entre las plantillas. Sirven como nombres de propiedad en los datos
estructurados del registro. Se recomienda el uso de la grafía Pascal para los nombres de los marcadores de
posición. Por ejemplo: {Count} , {FirstName} .
Cada mensaje de registro es un delegado Action que se mantiene en un campo estático creado por
LoggerMessage.Define . Por ejemplo, la aplicación de ejemplo crea un campo que describe un mensaje de registro
para una solicitud GET para la página de índice (Internal/LoggerExtensions.cs):

private static readonly Action<ILogger, Exception> _indexPageRequested;

Especifique lo siguiente para el delegado Action :


El nivel de registro.
Un identificador de evento único (EventId) con el nombre del método de extensión estático.
La plantilla de mensaje (cadena de formato con nombre).
Una solicitud para la página de índice de la aplicación de ejemplo establece:
El nivel de registro en Information .
El identificador de evento en 1 con el nombre del método IndexPageRequested .
La plantilla de mensaje (cadena de formato con nombre) en una cadena.

_indexPageRequested = LoggerMessage.Define(
LogLevel.Information,
new EventId(1, nameof(IndexPageRequested)),
"GET request for Index page");

Los almacenes de registro estructurado pueden usar el nombre de evento cuando se suministra con el
identificador de evento para enriquecer el registro. Por ejemplo, Serilog usa el nombre de evento.
El delegado Action se invoca mediante un método de extensión fuertemente tipado. El método
IndexPageRequested registra un mensaje para una solicitud GET de página de índice en la aplicación de ejemplo:

public static void IndexPageRequested(this ILogger logger)


{
_indexPageRequested(logger, null);
}

Se llama a IndexPageRequested en el registrador en el método OnGetAsync en Pages/Index.cshtml.cs:

public async Task OnGetAsync()


{
_logger.IndexPageRequested();

Quotes = await _db.Quotes.AsNoTracking().ToListAsync();


}

Inspeccione la salida de la consola de la aplicación:

info: LoggerMessageSample.Pages.IndexModel[1]
=> RequestId:0HL90M6E7PHK4:00000001 RequestPath:/ => /Index
GET request for Index page

Para pasar parámetros a un mensaje de registro, defina hasta seis tipos al crear el campo estático. La aplicación de
ejemplo registra una cadena cuando se agrega una cita. Para ello, define un tipo string para el campo Action :

private static readonly Action<ILogger, string, Exception> _quoteAdded;

La plantilla de mensaje de registro del delegado recibe sus valores de marcador de posición a partir de los tipos
proporcionados. La aplicación de ejemplo define un delegado para agregar una cita cuando el parámetro de la cita
es string :

_quoteAdded = LoggerMessage.Define<string>(
LogLevel.Information,
new EventId(2, nameof(QuoteAdded)),
"Quote added (Quote = '{Quote}')");

El método de extensión estático para agregar una cita, QuoteAdded , recibe el valor de argumento de la cita y lo pasa
al delegado Action :
public static void QuoteAdded(this ILogger logger, string quote)
{
_quoteAdded(logger, quote, null);
}

En el modelo de página para la página de índice (Pages/Index.cshtml.cs), se llama a QuoteAdded para registrar el
mensaje:

public async Task<IActionResult> OnPostAddQuoteAsync()


{
_db.Quotes.Add(Quote);
await _db.SaveChangesAsync();

_logger.QuoteAdded(Quote.Text);

return RedirectToPage();
}

Inspeccione la salida de la consola de la aplicación:

info: LoggerMessageSample.Pages.IndexModel[2]
=> RequestId:0HL90M6E7PHK5:0000000A RequestPath:/ => /Index
Quote added (Quote = 'You can avoid reality, but you cannot avoid the consequences of avoiding reality.
- Ayn Rand')

La aplicación de ejemplo implementa un patrón try – catch para la eliminación de la cita. Se registra un mensaje
informativo si se realiza correctamente una operación de eliminación. Se registra un mensaje de error para una
operación de eliminación si se produce una excepción. El mensaje de registro de la operación de eliminación con
error incluye el seguimiento de la pila de excepciones (Internal/LoggerExtensions.cs):

private static readonly Action<ILogger, string, int, Exception> _quoteDeleted;


private static readonly Action<ILogger, int, Exception> _quoteDeleteFailed;

_quoteDeleted = LoggerMessage.Define<string, int>(


LogLevel.Information,
new EventId(4, nameof(QuoteDeleted)),
"Quote deleted (Quote = '{Quote}' Id = {Id})");

_quoteDeleteFailed = LoggerMessage.Define<int>(
LogLevel.Error,
new EventId(5, nameof(QuoteDeleteFailed)),
"Quote delete failed (Id = {Id})");

Observe cómo se pasa la excepción al delegado en QuoteDeleteFailed :

public static void QuoteDeleted(this ILogger logger, string quote, int id)
{
_quoteDeleted(logger, quote, id, null);
}

public static void QuoteDeleteFailed(this ILogger logger, int id, Exception ex)
{
_quoteDeleteFailed(logger, id, ex);
}

En el modelo de página para la página de índice, una operación correcta de eliminación de cita llama al método
QuoteDeleted en el registrador. Cuando no se encuentra una cita para su eliminación, se produce una excepción
ArgumentNullException . La excepción se captura mediante la instrucción try – catch y se registra mediante una
llamada al método QuoteDeleteFailed en el registrador en el bloque catch (Pages/Index.cshtml.cs):

public async Task<IActionResult> OnPostDeleteQuoteAsync(int id)


{
var quote = await _db.Quotes.FindAsync(id);

// DO NOT use this approach in production code!


// You should check quote to see if it's null before removing
// it and saving changes to the database. A try-catch is used
// here for demonstration purposes of LoggerMessage features.
try
{
_db.Quotes.Remove(quote);
await _db.SaveChangesAsync();

_logger.QuoteDeleted(quote.Text, id);
}
catch (ArgumentNullException ex)
{
_logger.QuoteDeleteFailed(id, ex);
}

return RedirectToPage();
}

Cuando se elimine correctamente una cita, inspeccione la salida de la consola de la aplicación:

info: LoggerMessageSample.Pages.IndexModel[4]
=> RequestId:0HL90M6E7PHK5:00000016 RequestPath:/ => /Index
Quote deleted (Quote = 'You can avoid reality, but you cannot avoid the consequences of avoiding
reality. - Ayn Rand' Id = 1)

Cuando se produzca un error en la eliminación de una cita, inspeccione la salida de la consola de la aplicación.
Tenga en cuenta que la excepción se incluye en el mensaje del registro:

fail: LoggerMessageSample.Pages.IndexModel[5]
=> RequestId:0HL90M6E7PHK5:00000010 RequestPath:/ => /Index
Quote delete failed (Id = 999)
System.ArgumentNullException: Value cannot be null.
Parameter name: entity
at Microsoft.EntityFrameworkCore.Utilities.Check.NotNull[T](T value, String parameterName)
at Microsoft.EntityFrameworkCore.DbContext.Remove[TEntity](TEntity entity)
at Microsoft.EntityFrameworkCore.Internal.InternalDbSet`1.Remove(TEntity entity)
at LoggerMessageSample.Pages.IndexModel.<OnPostDeleteQuoteAsync>d__14.MoveNext() in
<PATH>\sample\Pages\Index.cshtml.cs:line 87

LoggerMessage.DefineScope
DefineScope(String) crea un delegado Func para definir un ámbito de registro. Las sobrecargas DefineScope
permiten pasar hasta tres parámetros de tipo a una cadena de formato con nombre (plantilla).
Al igual que sucede con el método Define , la cadena proporcionada al método DefineScope es una plantilla y no
una cadena interpolada. Los marcadores de posición se rellenan en el orden en que se especifican los tipos. Los
nombres de los marcadores de posición en la plantilla deben ser descriptivos y coherentes entre las plantillas.
Sirven como nombres de propiedad en los datos estructurados del registro. Se recomienda el uso de la grafía
Pascal para los nombres de los marcadores de posición. Por ejemplo: {Count} , {FirstName} .
Defina un ámbito de registro para aplicarlo a una serie de mensajes de registro mediante el método
DefineScope(String).
La aplicación de ejemplo tiene un botón Borrar todo para eliminar todas las citas de la base de datos. Para
eliminar las citas, se van quitando de una en una. Cada vez que se elimina una cita, se llama al método
QuoteDeleted en el registrador. Se agrega un ámbito de registro a estos mensajes de registro.

Habilite IncludeScopes en las opciones del registrador de la consola:

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>


WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();

Es necesario establecer IncludeScopes en las aplicaciones de ASP.NET Core 2.0 para habilitar los ámbitos de
registro. Está previsto incluir en la versión ASP.NET Core 2.1 la opción de establecer IncludeScopes a través de
archivos de configuración appsettings.
La aplicación de ejemplo borra los demás proveedores y agrega filtros para reducir la salida del registro. Así
resulta más fácil ver los mensajes de registro del ejemplo que muestran las características de LoggerMessage .
Para crear un ámbito de registro, agregue un campo para que contenga un delegado Func para el ámbito. La
aplicación de ejemplo crea un campo denominado _allQuotesDeletedScope (Internal/LoggerExtensions.cs):

private static Func<ILogger, int, IDisposable> _allQuotesDeletedScope;

Use DefineScope para crear el delegado. Pueden especificarse hasta tres tipos para usarlos como argumentos de
plantilla cuando se invoca el delegado. La aplicación de ejemplo usa una plantilla de mensaje que incluye el
número de citas eliminadas (un tipo int ):

_allQuotesDeletedScope = LoggerMessage.DefineScope<int>("All quotes deleted (Count = {Count})");

Proporcione un método de extensión estático para el mensaje de registro. Incluya todos los parámetros de tipo
para propiedades con nombre que aparezcan en la plantilla de mensaje. La aplicación de ejemplo toma un valor de
número count de citas que se van a eliminar y devuelve _allQuotesDeletedScope :

public static IDisposable AllQuotesDeletedScope(this ILogger logger, int count)


{
return _allQuotesDeletedScope(logger, count);
}

El ámbito encapsula las llamadas a la extensión de registro en un bloque using :


public async Task<IActionResult> OnPostDeleteAllQuotesAsync()
{
var quoteCount = await _db.Quotes.CountAsync();

using (_logger.AllQuotesDeletedScope(quoteCount))
{
foreach (Quote quote in _db.Quotes)
{
_db.Quotes.Remove(quote);

_logger.QuoteDeleted(quote.Text, quote.Id);
}
await _db.SaveChangesAsync();
}

return RedirectToPage();
}

Inspeccione los mensajes de registro en la salida de la consola de la aplicación. En el resultado siguiente se


muestran tres citas eliminadas con el mensaje del ámbito de registro incluido:

info: LoggerMessageSample.Pages.IndexModel[4]
=> RequestId:0HL90M6E7PHK5:0000002E RequestPath:/ => /Index => All quotes deleted (Count = 3)
Quote deleted (Quote = 'Quote 1' Id = 2)
info: LoggerMessageSample.Pages.IndexModel[4]
=> RequestId:0HL90M6E7PHK5:0000002E RequestPath:/ => /Index => All quotes deleted (Count = 3)
Quote deleted (Quote = 'Quote 2' Id = 3)
info: LoggerMessageSample.Pages.IndexModel[4]
=> RequestId:0HL90M6E7PHK5:0000002E RequestPath:/ => /Index => All quotes deleted (Count = 3)
Quote deleted (Quote = 'Quote 3' Id = 4)

Vea también
Registro
Controlar errores en ASP.NET Core
25/05/2018 • 10 minutes to read • Edit Online

Por Steve Smith y Tom Dykstra


Este artículo trata sobre los métodos comunes para controlar errores en aplicaciones ASP.NET Core.
Vea o descargue el código de ejemplo (cómo descargarlo)

Página de excepciones para el desarrollador


Para configurar una aplicación de modo que muestre una página con información detallada sobre las
excepciones, instale el paquete NuGet Microsoft.AspNetCore.Diagnostics y agregue una línea al método
Configure de la clase Startup:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)


{
loggerFactory.AddConsole();
env.EnvironmentName = EnvironmentName.Production;
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/error");
}

Coloque UseDeveloperExceptionPage antes del software intermedio en el que quiera capturar excepciones, como
app.UseMvc .

WARNING
Habilite la página de excepciones para el desarrollador solo cuando la aplicación se ejecute en el entorno de
desarrollo. No le interesa compartir públicamente información detallada sobre las excepciones cuando la aplicación se
ejecute en producción. Más información sobre la configuración de entornos.

Para ver la página de excepciones para el desarrollador, ejecute la aplicación de ejemplo con el entorno
establecido en Development y agregue ?throw=true a la URL base de la aplicación. La página incluye varias
pestañas con información sobre la excepción y la solicitud. La primera pestaña incluye un seguimiento de la pila.
En la pestaña siguiente se muestran los parámetros de cadena de consulta, si los hay.

Esta solicitud no tenía cookies, pero en caso de que las tuviera, aparecerían en la pestaña Cookies. Puede ver los
encabezados que se han pasado en la última pestaña.
Configuración de una página personalizada de control de excepciones
Se recomienda configurar una página de controlador de excepciones para usarla cuando la aplicación no se
ejecute en el entorno Development .

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)


{
loggerFactory.AddConsole();
env.EnvironmentName = EnvironmentName.Production;
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/error");
}

En una aplicación MVC, no decore explícitamente el método de acción del controlador de errores con atributos
de método HTTP, como HttpGet . El uso de verbos explícitos podría impedir que algunas solicitudes llegasen al
método.

[Route("/Error")]
public IActionResult Index()
{
// Handle error here
}
Configuración de páginas de códigos de estado
Una aplicación no proporciona de forma predeterminada una página de códigos de estado enriquecida para
códigos de estado HTTP como 404 No encontrado. Para proporcionarlas, configure el middleware de páginas
de código de estado agregando una línea al método Startup.Configure :

app.UseStatusCodePages();

El middleware de páginas de código de estado agrega de forma predeterminada controladores simples de solo
texto para códigos de estado comunes, como 404:

El middleware admite diversos métodos de extensión. Uno de ellos es una expresión lambda:

app.UseStatusCodePages(async context =>


{
context.HttpContext.Response.ContentType = "text/plain";
await context.HttpContext.Response.WriteAsync(
"Status code page, status code: " +
context.HttpContext.Response.StatusCode);
});

Otro toma un tipo de contenido y una cadena de formato:

app.UseStatusCodePages("text/plain", "Status code page, status code: {0}");

También hay métodos de extensión de redireccionamiento y de nueva ejecución. El método de


redireccionamiento envía al cliente un código de estado 302:

app.UseStatusCodePagesWithRedirects("/error/{0}");

El método de nueva ejecución devuelve el código de estado original al cliente, pero también ejecuta el
controlador para la dirección URL de redireccionamiento:

app.UseStatusCodePagesWithReExecute("/error/{0}");

Las páginas de códigos de estado se pueden deshabilitar en solicitudes específicas en un método de controlador
de páginas de Razor o en un controlador MVC. Para deshabilitar páginas de códigos de estado, intente
recuperar IStatusCodePagesFeature de la colección HttpContext.Features de la solicitud y deshabilite la
característica si está disponible:
var statusCodePagesFeature = HttpContext.Features.Get<IStatusCodePagesFeature>();

if (statusCodePagesFeature != null)
{
statusCodePagesFeature.Enabled = false;
}

Código de control de excepciones


El código de las páginas de control de excepciones puede producir excepciones. Es recomendable que las
páginas de errores de producción incluyan únicamente contenido estático.
Además, tenga en cuenta que una vez que se hayan enviado los encabezados de una respuesta, no se podrá
cambiar el código de estado de la respuesta ni se podrá ejecutar ninguna página de excepción o controlador.
Deberá completarse la respuesta o anularse la conexión.

Control de excepciones del servidor


Además de la lógica de control de excepciones de la aplicación, el servidor que hospede la aplicación llevará a
cabo el control de algunas excepciones. Si el servidor detecta una excepción antes de que se envíen los
encabezados, enviará una respuesta 500 Error interno del servidor sin cuerpo. Si el servidor detecta una
excepción después de que se hayan enviado los encabezados, cerrará la conexión. El servidor controla las
solicitudes que no controla la aplicación. El control de excepciones del servidor controla todas las excepciones
que se producen. Las páginas de error personalizadas y el software intermedio o filtros de control de
excepciones que se hayan configurado no afectarán a este comportamiento.

Control de excepciones de inicio


Solo el nivel de hospedaje puede controlar las excepciones que tienen lugar durante el inicio de la aplicación.
Mediante el host de web también puede configurar cómo se comporta el host en respuesta a los errores durante
el inicio con las claves captureStartupErrors y detailedErrors .
El hospedaje solo puede mostrar una página de error para un error de inicio capturado si este se produce
después del enlace de puerto/dirección del host. Si se produce un error en algún enlace por cualquier motivo, el
nivel de hospedaje registra una excepción crítica, el proceso de dotnet se bloquea y no se muestra ninguna
página de error si la aplicación se está ejecutando en el servidor de Kestrel.
Si se ejecuta en IIS o IIS Express, el módulo ASP.NET Core devuelve un error de proceso 502.5 en caso de que el
proceso no se pueda iniciar. Siga los consejos para solucionar problemas del tema Troubleshoot ASP.NET Core
on IIS (Solución de problemas de ASP.NET Core en IIS ).

Control de errores de ASP.NET MVC


Las aplicaciones MVC tienen algunas opciones adicionales para controlar errores, como la configuración de
filtros de excepciones y la realización de la validación del modelo.
Filtros de excepciones
Los filtros de excepciones se pueden configurar globalmente, o bien por controlador o por acción, en una
aplicación MVC. Estos filtros controlan todas las excepciones no controladas que se hayan producido durante la
ejecución de una acción de controlador o de otro filtro; en caso contrario, no se les llama. Encontrará más
información sobre los filtros de excepciones en Filtros.
TIP
Los filtros de excepciones están indicados para capturar las excepciones que se producen en las acciones de MVC, pero no
son tan flexibles como el software intermedio de control de errores. Use de preferencia el software intermedio para los
casos generales y aplique filtros solo cuando deba realizar el control de errores de manera diferente según la acción de
MVC elegida.

Control de errores de estado del modelo


La validación de modelos se produce antes de invocar cada acción de controlador, y es el método de acción el
encargado de inspeccionar ModelState.IsValid y reaccionar de manera apropiada.
Algunas aplicaciones optarán por seguir una convención estándar para tratar los errores de validación de
modelos, en cuyo caso un filtro podría ser el lugar adecuado para implementar esta directiva. Debe probar cómo
se comportan las acciones con estados de modelo no válidos. Obtenga más información en Probar la lógica del
controlador.
Proveedores de archivo en ASP.NET Core
25/06/2018 • 12 minutes to read • Edit Online

Por Steve Smith


ASP.NET Core abstrae el acceso al sistema de archivos mediante el uso de proveedores de archivos.
Vea o descargue el código de ejemplo (cómo descargarlo)

Abstracciones de proveedores de archivos


Los proveedores de archivos son una abstracción respecto a los sistemas de archivos. La interfaz principal es
IFileProvider . IFileProvider expone métodos para obtener información de archivos ( IFileInfo ) y de
directorios ( IDirectoryContents ), y para configurar las notificaciones de cambio (con IChangeToken ).
IFileInfo proporciona métodos y propiedades de archivos o directorios individuales. Tiene dos propiedades
booleanas, Exists y IsDirectory , así como propiedades que describen el archivo, como Name , Length (en bytes)
y la fecha LastModified . Se puede leer en el archivo mediante el método CreateReadStream .

Implementaciones del proveedor de archivos


Hay disponibles tres implementaciones de IFileProvider : física, insertada y compuesta. El proveedor físico se
utiliza para tener acceso a los archivos del sistema real. El proveedor insertado se utiliza para tener acceso a
archivos insertados en ensamblados. El proveedor compuesto se utiliza para proporcionar acceso combinado a
archivos y directorios de uno o más proveedores.
PhysicalFileProvider
PhysicalFileProvider proporciona acceso al sistema de archivos físico. Encapsula el tipo System.IO.File (para el
proveedor físico), definiendo el ámbito de todas las rutas de acceso a un directorio y sus elementos secundarios.
Este ámbito limita el acceso a un directorio determinado y sus elementos secundarios, impidiendo el acceso al
sistema de archivos fuera de este límite. Al crear una instancia de este proveedor, debe proporcionarle una ruta de
acceso de directorio, que actúa como la ruta de acceso base para todas las solicitudes realizadas a este proveedor
(y que restringe el acceso fuera de esta ruta de acceso). En una aplicación ASP.NET Core, puede crear instancias de
un proveedor PhysicalFileProvider directamente o puede solicitar un proveedor IFileProvider de un
controlador o constructor del servicio a través de inserción de dependencias. Por lo general, el último enfoque
brindará una solución más flexible y más fácil de probar.
En el ejemplo siguiente se muestra cómo crear PhysicalFileProvider .

IFileProvider provider = new PhysicalFileProvider(applicationRoot);


IDirectoryContents contents = provider.GetDirectoryContents(""); // the applicationRoot contents
IFileInfo fileInfo = provider.GetFileInfo("wwwroot/js/site.js"); // a file under applicationRoot

Para recorrer en iteración el contenido del directorio u obtener información de un archivo específico, puede
proporcionar una subruta de acceso.
Para solicitar un proveedor de un controlador, especifíquelo en el constructor del controlador y asígnelo a un
campo local. Utilice la instancia local de los métodos de acción:
public class HomeController : Controller
{
private readonly IFileProvider _fileProvider;

public HomeController(IFileProvider fileProvider)


{
_fileProvider = fileProvider;
}

public IActionResult Index()


{
var contents = _fileProvider.GetDirectoryContents("");
return View(contents);
}

A continuación, cree el proveedor en la clase Startup de la aplicación:

using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging;

namespace FileProviderSample
{
public class Startup
{
private IHostingEnvironment _hostingEnvironment;
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
Configuration = builder.Build();

_hostingEnvironment = env;
}

public IConfigurationRoot Configuration { get; }

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddMvc();

var physicalProvider = _hostingEnvironment.ContentRootFileProvider;


var embeddedProvider = new EmbeddedFileProvider(Assembly.GetEntryAssembly());
var compositeProvider = new CompositeFileProvider(physicalProvider, embeddedProvider);

// choose one provider to use for the app and register it


//services.AddSingleton<IFileProvider>(physicalProvider);
//services.AddSingleton<IFileProvider>(embeddedProvider);
services.AddSingleton<IFileProvider>(compositeProvider);
}

En la vista Index.cshtml, recorrer en iteración el IDirectoryContents proporcionado:


@using Microsoft.Extensions.FileProviders
@model IDirectoryContents

<h2>Folder Contents</h2>

<ul>
@foreach (IFileInfo item in Model)
{
if (item.IsDirectory)
{
<li><strong>@item.Name</strong></li>
}
else
{
<li>@item.Name - @item.Length bytes</li>
}
}
</ul>

Resultado:

EmbeddedFileProvider
EmbeddedFileProvider se utiliza para tener acceso a archivos insertados en ensamblados. En .NET Core, se insertan
archivos en un ensamblado con el elemento <EmbeddedResource> en el archivo .csproj:

<ItemGroup>
<EmbeddedResource Include="Resource.txt;**\*.js"
Exclude="bin\**;obj\**;**\*.xproj;packages\**;@(EmbeddedResource)" />
<Content Update="wwwroot\**\*;Views\**\*;Areas\**\Views;appsettings.json;web.config">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
</ItemGroup>

Puede usar patrones de comodines para especificar archivos que se insertarán en el ensamblado. Estos patrones
se pueden usar para que coincidan con uno o más archivos.

NOTE
Es improbable que alguna vez quiera insertar cada archivo .js del proyecto en su ensamblado; el ejemplo anterior se incluye
solo con fines de demostración.

Al crear EmbeddedFileProvider , pase el ensamblado que va a leer a su constructor.

var embeddedProvider = new EmbeddedFileProvider(Assembly.GetEntryAssembly());

El fragmento de código anterior muestra cómo crear EmbeddedFileProvider con acceso al ensamblado que se está
ejecutando actualmente.
La actualización de la aplicación de ejemplo para usar EmbeddedFileProvider genera la siguiente salida:

NOTE
Los recursos insertados no exponen directorios. En su lugar, la ruta de acceso al recurso (a través de su espacio de nombres)
se inserta en su nombre de archivo con separadores . .

TIP
El constructor EmbeddedFileProvider acepta un parámetro baseNamespace opcional. Si se especifica este parámetro, las
llamadas a GetDirectoryContents tendrán como ámbito esos recursos en el espacio de nombres proporcionado.

CompositeFileProvider
CompositeFileProvider combina instancias de IFileProvider , y expone una única interfaz para trabajar con
archivos de varios proveedores. Al crear CompositeFileProvider , se pasan una o varias instancias de
IFileProvider a su constructor:

var physicalProvider = _hostingEnvironment.ContentRootFileProvider;


var embeddedProvider = new EmbeddedFileProvider(Assembly.GetEntryAssembly());
var compositeProvider = new CompositeFileProvider(physicalProvider, embeddedProvider);

Actualizar la aplicación de ejemplo para que use un proveedor CompositeFileProvider que incluya los proveedores
físicos y los insertados que se configuraron anteriormente, produce la siguiente salida:

Observación de cambios
El método IFileProvider Watch proporciona una manera de ver uno o más archivos o directorios para detectar
cambios. Este método acepta una cadena de ruta de acceso, que puede usar patrones de comodines para
especificar varios archivos y devuelve un token IChangeToken . Este token expone una propiedad HasChanged que
se puede inspeccionar, así como un método RegisterChangeCallback al que se llama cuando se detectan cambios
en la cadena de ruta de acceso especificada. Tenga en cuenta que cada token de cambio solo llama a su devolución
de llamada asociada en respuesta a un único cambio. Para habilitar la supervisión constante, puede usar
TaskCompletionSource tal y como se muestra a continuación, o volver a crear instancias de IChangeToken en
respuesta a los cambios.
En el ejemplo de este artículo, se configura una aplicación de consola para mostrar un mensaje cada vez que se
modifica un archivo de texto:

private static PhysicalFileProvider _fileProvider =


new PhysicalFileProvider(Directory.GetCurrentDirectory());

public static void Main(string[] args)


{
Console.WriteLine("Monitoring quotes.txt for changes (Ctrl-c to quit)...");

while (true)
{
MainAsync().GetAwaiter().GetResult();
}
}

private static async Task MainAsync()


{
IChangeToken token = _fileProvider.Watch("quotes.txt");
var tcs = new TaskCompletionSource<object>();

token.RegisterChangeCallback(state =>
((TaskCompletionSource<object>)state).TrySetResult(null), tcs);

await tcs.Task.ConfigureAwait(false);

Console.WriteLine("quotes.txt changed");
}

El resultado, después de guardar el archivo varias veces:

NOTE
Algunos sistemas de archivos, como contenedores de Docker y recursos compartidos de red, no pueden enviar notificaciones
de cambio de forma confiable. Establezca la variable de entorno DOTNET_USE_POLLINGFILEWATCHER en 1 o true para
sondear el sistema de archivos en busca de cambios cada 4 segundos.
Patrones de comodines
Las rutas de acceso del sistema de archivos utilizan patrones de caracteres comodín denominados patrones de
comodines. Estos sencillos modelos se pueden usar para especificar grupos de archivos. Los dos caracteres
comodín son * y ** .
*

Coincide con cualquier elemento del nivel de carpeta actual, con cualquier nombre de archivo o con cualquier
extensión de archivo. Las coincidencias finalizan con los caracteres / y . en la ruta de acceso de archivo.
**

Coincide con cualquier elemento de varios niveles de directorios. Puede usarse para coincidir de forma recursiva
con muchos archivos dentro de una jerarquía de directorios.
Ejemplos de patrones de uso de comodines
directory/file.txt

Coincide con un archivo concreto en un directorio específico.


directory/*.txt

Coincide con todos los archivos que tengan la extensión .txt en un directorio específico.
directory/*/bower.json

Coincide con todos los archivos bower.json que estén en directorios exactamente un nivel por debajo del
directorio directory .
directory/**/*.txt

Coincide con todos los archivos cuya extensión sea .txt y se encuentren en cualquier lugar del directorio
directory .

Uso de proveedor de archivos en ASP.NET Core


Varias partes de ASP.NET Core usan proveedores de archivos. IHostingEnvironment expone la raíz del contenido
de la aplicación y la raíz web como tipos IFileProvider . El middleware de archivos estáticos usa proveedores de
archivos para buscar archivos estáticos. Razor hace un uso intensivo de IFileProvider en la localización de vistas.
La funcionalidad de publicación de Dotnet usa proveedores de archivos y patrones de comodines para especificar
los archivos que deben publicarse.

Recomendaciones para su uso en aplicaciones


Si su aplicación de ASP.NET Core requiere acceso al sistema de archivos, puede solicitar una instancia de
IFileProvider a través de la inserción de dependencias y, a continuación, utilizar sus métodos para realizar el
acceso, tal como se muestra en este ejemplo. Esto le permite configurar el proveedor una sola vez cuando se inicia
la aplicación, y reduce el número de tipos de implementación de los que la aplicación crea instancias.
Hospedaje en ASP.NET Core
21/06/2018 • 2 minutes to read • Edit Online

Las aplicaciones .NET configuran e inician un host. El host es responsable de la administración del inicio y la
duración de la aplicación. Hay dos API host disponibles para su uso:
Host web: indicado para el hospedaje de aplicaciones web.
Host genérico (ASP.NET Core 2.1 y versiones posteriores): indicado para el hospedaje de aplicaciones que
no sean web, por ejemplo, las que ejecutan tareas en segundo plano. En una versión posterior, el host
genérico será adecuado para hospedar aplicaciones de cualquier tipo, incluidas las web. Llegado el
momento, el host genérico reemplazará el host web.
Para hospedar aplicaciones web ASP.NET Core, los desarrolladores deben usar el host web basado en
WebHostBuilder. Para hospedar aplicaciones que no sean web, los desarrolladores deben usar el host
genérico basado en HostBuilder.
Host web de ASP.NET Core
25/05/2018 • 33 minutes to read • Edit Online

Por Luke Latham


Las aplicaciones de ASP.NET Core configuran e inician un host. El host es responsable de la administración del
inicio y la duración de la aplicación. Como mínimo, el host configura un servidor y una canalización de
procesamiento de solicitudes. En este tema se aborda el host web de ASP.NET Core (WebHostBuilder), muy útil
para hospedar aplicaciones web. Para la cobertura del host genérico de .NET (HostBuilder), consulte el tema Host
genérico.

Configuración de un host
ASP.NET Core 2.x
ASP.NET Core 1.x
Cree un host con una instancia de WebHostBuilder. Normalmente, esto se realiza en el punto de entrada de la
aplicación, el método Main . En las plantillas de proyecto, Main se encuentra en Program.cs. Los archivos
Program.cs estándar llaman a CreateDefaultBuilder para empezar a configurar un host:

public class Program


{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>


WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
}

CreateDefaultBuilder realiza las tareas siguientes:


Configura Kestrel como el servidor web. Para conocer las opciones predeterminadas de Kestrel, consulte la
sección Opciones de Kestrel de la implementación de servidor web Kestrel en ASP.NET Core.
Establece la raíz de contenido en la ruta de acceso devuelta por Directory.GetCurrentDirectory.
Carga configuración opcional de:
appsettings.json.
appsettings.{Environment}.json.
Secretos del usuario cuando la aplicación se ejecuta en el entorno Development .
Variables de entorno.
Argumentos de la línea de comandos.
Configura el registro para la salida de consola y de depuración. El registro incluye reglas de filtrado del
registro especificadas en una sección de configuración de registro de un archivo appSettings.json o
appsettings.{Environment}.json.
Cuando se ejecuta detrás de IIS, permite la integración con IIS. Configura la ruta de acceso base y el puerto
que escucha el servidor cuando se usa el módulo ASP.NET Core. El módulo crea a un proxy inverso entre IIS y
Kestrel. También configura la aplicación para que capture errores de inicio. Para conocer las opciones
predeterminadas de IIS, consulte la sección Opciones de IIS de Hospedaje de ASP.NET Core en Windows con
IIS.
Establece ServiceProviderOptions.ValidateScopes en true si el entorno de la aplicación es desarrollo. Para
más información, vea Validación del ámbito.
La raíz del contenido determina la ubicación en la que el host busca archivos de contenido como, por ejemplo,
archivos de vista MVC. Cuando la aplicación se inicia desde la carpeta raíz del proyecto, esta se utiliza como la
raíz del contenido. Este es el valor predeterminado usado en Visual Studio y las nuevas plantillas dotnet.
Para más información sobre la configuración de la aplicación, consulte Configuración en ASP.NET Core.

NOTE
Como alternativa al uso del método estático CreateDefaultBuilder , crear un host de WebHostBuilder es un enfoque
compatible con ASP.NET Core 2.x. Para más información, vea la pestaña ASP.NET Core 1.x.

Al configurar un host, se pueden proporcionar los métodos Configure y ConfigureServices. Si se especifica una
clase Startup , debe definir un método Configure . Para obtener más información, vea Application Startup in
ASP.NET Core (Inicio de la aplicación en ASP.NET Core). Varias llamadas a ConfigureServices se anexan entre sí.
Varias llamadas a Configure o UseStartup en el WebHostBuilder reemplazan la configuración anterior.

Valores de configuración de host


WebHostBuilder se basa en los siguientes métodos para establecer los valores de configuración del host:
Configuración del generador de host, que incluye las variables de entorno con el formato
ASPNETCORE_{configurationKey} . Por ejemplo: ASPNETCORE_ENVIRONMENT .
Métodos explícitos, como HostingAbstractionsWebHostBuilderExtensions.UseContentRoot.
UseSetting y la clave asociada. Al establecer un valor con UseSetting , el valor se establece como una cadena,
independientemente del tipo.
El host usa cualquier opción que establece un valor en último lugar. Para obtener más información, consulte
Invalidación de la configuración en la sección siguiente.
Capturar errores de inicio
Esta configuración controla la captura de errores de inicio.
Clave: captureStartupErrors
Tipo: bool ( true o 1 )
Valor predeterminado: false , a menos que la aplicación se ejecute con Kestrel detrás de IIS, en cuyo caso el
valor predeterminado es true .
Establecer mediante: CaptureStartupErrors
Variable de entorno: ASPNETCORE_CAPTURESTARTUPERRORS
Cuando es false , los errores durante el inicio provocan la salida del host. Cuando es true , el host captura las
excepciones producidas durante el inicio e intenta iniciar el servidor.
ASP.NET Core 2.x
ASP.NET Core 1.x

WebHost.CreateDefaultBuilder(args)
.CaptureStartupErrors(true)

Raíz del contenido


Esta configuración determina la ubicación en la que ASP.NET Core comienza a buscar archivos de contenido,
como las vistas MVC.
Clave: contentRoot
Tipo: cadena
Valor predeterminado: la carpeta donde se encuentra el ensamblado de la aplicación.
Establecer mediante: UseContentRoot
Variable de entorno: ASPNETCORE_CONTENTROOT
La raíz de contenido también se usa como la ruta de acceso base para la configuración de Raíz web. Si no existe
la ruta de acceso, el host no se puede iniciar.
ASP.NET Core 2.x
ASP.NET Core 1.x

WebHost.CreateDefaultBuilder(args)
.UseContentRoot("c:\\<content-root>")

Errores detallados
Determina si se deben capturar los errores detallados.
Clave: detailedErrors
Tipo: bool ( true o 1 )
Valor predeterminado: false
Establecer mediante: UseSetting
Variable de entorno: ASPNETCORE_DETAILEDERRORS
Cuando se habilita (o cuando el entorno está establecido en Development ), la aplicación captura excepciones
detalladas.
ASP.NET Core 2.x
ASP.NET Core 1.x

WebHost.CreateDefaultBuilder(args)
.UseSetting(WebHostDefaults.DetailedErrorsKey, "true")

Entorno
Establece el entorno de la aplicación.
Clave: environment
Tipo: cadena
Valor predeterminado: producción
Establecer mediante: UseEnvironment
Variable de entorno: ASPNETCORE_ENVIRONMENT
El entorno se puede establecer en cualquier valor. Los valores definidos por el marco son Development , Staging
y Production . Los valores no distinguen mayúsculas de minúsculas. De forma predeterminada, el entorno se lee
desde la variable de entorno ASPNETCORE_ENVIRONMENT . Cuando se usa Visual Studio, las variables de entorno se
pueden establecer en el archivo launchSettings.json. Para obtener más información, consulte Uso de varios
entornos.
ASP.NET Core 2.x
ASP.NET Core 1.x
WebHost.CreateDefaultBuilder(args)
.UseEnvironment(EnvironmentName.Development)

Ensamblados de inicio de hospedaje


Establece los ensamblados de inicio de hospedaje de la aplicación.
Clave: hostingStartupAssemblies
Tipo: cadena
Valor predeterminado: una cadena vacía
Establecer mediante: UseSetting
Variable de entorno: ASPNETCORE_HOSTINGSTARTUPASSEMBLIES
Una cadena delimitada por punto y coma de ensamblados de inicio de hospedaje para cargar en el inicio. Esta
característica es nueva en ASP.NET Core 2.0.
Aunque el valor de configuración predeterminado es una cadena vacía, los ensamblados de inicio de hospedaje
incluyen siempre el ensamblado de la aplicación. Cuando se especifican los ensamblados de inicio de hospedaje,
estos se agregan al ensamblado de la aplicación para que se carguen cuando la aplicación genera sus servicios
comunes durante el inicio.
ASP.NET Core 2.x
ASP.NET Core 1.x

WebHost.CreateDefaultBuilder(args)
.UseSetting(WebHostDefaults.HostingStartupAssembliesKey, "assembly1;assembly2")

Preferir las direcciones URL de hospedaje


Indica si el host debe escuchar en las direcciones URL configuradas con WebHostBuilder en lugar de las que
estén configuradas con la implementación de IServer .
Clave: preferHostingUrls
Tipo: bool ( true o 1 )
Valor predeterminado: true
Establecer mediante: PreferHostingUrls
Variable de entorno: ASPNETCORE_PREFERHOSTINGURLS
Esta característica es nueva en ASP.NET Core 2.0.
ASP.NET Core 2.x
ASP.NET Core 1.x

WebHost.CreateDefaultBuilder(args)
.PreferHostingUrls(false)

Evitar el inicio de hospedaje


Impide la carga automática de los ensamblados de inicio de hospedaje, incluidos los configurados por el
ensamblado de la aplicación. Para más información, vea Enhance an app from an external assembly with
IHostingStartup (Mejora de una aplicación desde un ensamblado externo con IHostingStartup).
Clave: preventHostingStartup
Tipo: bool ( true o 1 )
Valor predeterminado: false
Establecer mediante: UseSetting
Variable de entorno: ASPNETCORE_PREVENTHOSTINGSTARTUP

Esta característica es nueva en ASP.NET Core 2.0.


ASP.NET Core 2.x
ASP.NET Core 1.x

WebHost.CreateDefaultBuilder(args)
.UseSetting(WebHostDefaults.PreventHostingStartupKey, "true")

Direcciones URL de servidor


Indica las direcciones IP o las direcciones de host con los puertos y protocolos en que el servidor debe escuchar
las solicitudes.
Clave: urls
Tipo: cadena
Predeterminado: http://localhost:5000
Establecer mediante: UseUrls
Variable de entorno: ASPNETCORE_URLS
Se establece una lista de prefijos de URL separados por punto y coma (;) a los que debe responder el servidor.
Por ejemplo: http://localhost:123 . Use "*" para indicar que el servidor debe escuchar las solicitudes en
cualquier dirección IP o nombre de host con el puerto y el protocolo especificados (por ejemplo, http://*:5000 ).
El protocolo ( http:// o https:// ) debe incluirse con cada dirección URL. Los formatos admitidos varían en
función del servidor.
ASP.NET Core 2.x
ASP.NET Core 1.x

WebHost.CreateDefaultBuilder(args)
.UseUrls("http://*:5000;http://localhost:5001;https://hostname:5002")

Kestrel tiene su propia API de configuración de punto de conexión. Para más información, vea Kestrel web server
implementation in ASP.NET Core (Implementación del servidor web de Kestrel en ASP.NET Core).
Tiempo de espera de apagado
Especifica la cantidad de tiempo que se espera hasta el cierre del host de web.
Clave: shutdownTimeoutSeconds
Tipo: int
Valor predeterminado: 5
Establecer mediante: UseShutdownTimeout
Variable de entorno: ASPNETCORE_SHUTDOWNTIMEOUTSECONDS
Aunque la clave acepta un int con UseSetting(por ejemplo,
.UseSetting(WebHostDefaults.ShutdownTimeoutKey, "10") ), el método de extensión UseShutdownTimeout toma
TimeSpan. Esta característica es nueva en ASP.NET Core 2.0.
Durante el período de tiempo de espera, el hospedaje:
Activa IApplicationLifetime.ApplicationStopping.
Trata de detener los servicios hospedados y registra cualquier error que se produzca en los servicios que no
se puedan detener.
Si el período de tiempo de espera expira antes de que todos los servicios hospedados se hayan detenido, los
servicios activos que queden se detendrán cuando se cierre la aplicación. Los servicios se detienen aun cuando
no hayan terminado de procesarse. Si los servicios requieren más tiempo para detenerse, aumente el valor de
tiempo de espera.
ASP.NET Core 2.x
ASP.NET Core 1.x

WebHost.CreateDefaultBuilder(args)
.UseShutdownTimeout(TimeSpan.FromSeconds(10))

Ensamblado de inicio
Determina el ensamblado en el que se va a buscar la clase Startup .
Clave: startupAssembly
Tipo: cadena
Valor predeterminado: el ensamblado de la aplicación
Establecer mediante: UseStartup
Variable de entorno: ASPNETCORE_STARTUPASSEMBLY
Se puede hacer referencia al ensamblado por su nombre ( string ) o su tipo ( TStartup ). Si se llama a varios
métodos UseStartup , la última llamada tiene prioridad.
ASP.NET Core 2.x
ASP.NET Core 1.x

WebHost.CreateDefaultBuilder(args)
.UseStartup("StartupAssemblyName")

WebHost.CreateDefaultBuilder(args)
.UseStartup<TStartup>()

Raíz web
Establece la ruta de acceso relativa a los recursos estáticos de la aplicación.
Clave: webroot
Tipo: cadena
Valor predeterminado: si no se especifica, el valor predeterminado es "(Raíz de contenido)/wwwroot", si existe
la ruta de acceso. Si la ruta de acceso no existe, se utiliza un proveedor de archivos no-op.
Establecer mediante: UseWebRoot
Variable de entorno: ASPNETCORE_WEBROOT
ASP.NET Core 2.x
ASP.NET Core 1.x

WebHost.CreateDefaultBuilder(args)
.UseWebRoot("public")

Invalidación de la configuración
Use Configuración para configurar el host. En el ejemplo siguiente, la configuración del host se especifica de
forma opcional en un archivo hosting.json. Cualquier configuración que se carga desde el archivo hosting.json
puede reemplazarse por argumentos de línea de comandos. La configuración generada (en config ) se utiliza
para configurar el host con UseConfiguration .
ASP.NET Core 2.x
ASP.NET Core 1.x
hosting.json:

{
urls: "http://*:5005"
}

Invalidar la configuración proporcionada por UseUrls primero con la configuración de hosting.json y segundo
con la configuración del argumento de línea de comandos:

public class Program


{
public static void Main(string[] args)
{
BuildWebHost(args).Run();
}

public static IWebHost BuildWebHost(string[] args)


{
var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("hosting.json", optional: true)
.AddCommandLine(args)
.Build();

return WebHost.CreateDefaultBuilder(args)
.UseUrls("http://*:5000")
.UseConfiguration(config)
.Configure(app =>
{
app.Run(context =>
context.Response.WriteAsync("Hello, World!"));
})
.Build();
}
}

NOTE
El método de extensión UseConfiguration no es capaz de analizar actualmente una sección de configuración devuelta por
GetSection (por ejemplo, .UseConfiguration(Configuration.GetSection("section")) . El método GetSection filtra
las claves de configuración a la sección solicitada, pero deja el nombre de sección en las claves (por ejemplo, section:urls
, section:environment ). El método UseConfiguration espera que las claves coincidan con las claves WebHostBuilder
(por ejemplo, urls , environment ). La presencia del nombre de sección en las claves evita que los valores de la sección
configuren el host. Este problema se corregirá en una versión futura. Para obtener más información y soluciones
alternativas, consulte Passing configuration section into WebHostBuilder.UseConfiguration uses full keys (Pasar la sección de
configuración a WebHostBuilder.UseConfiguration usa claves completas).

Para especificar el host que se ejecuta en una dirección URL determinada, se puede pasar el valor deseado desde
un símbolo del sistema al ejecutar dotnet run. El argumento de línea de comandos reemplaza el valor urls del
archivo hosting.json, y el servidor escucha en el puerto 8080:

dotnet run --urls "http://*:8080"


Administración del host
ASP.NET Core 2.x
ASP.NET Core 1.x
Run
El método Run inicia la aplicación web y bloquea el subproceso que realiza la llamada hasta que se apague el
host:

host.Run();

Start
Ejecute el host de manera que se evite un bloqueo mediante una llamada a su método Start :

using (host)
{
host.Start();
Console.ReadLine();
}

Si se pasa una lista de direcciones URL al método Start , la escucha se produce en las direcciones URL
especificadas:

var urls = new List<string>()


{
"http://*:5000",
"http://localhost:5001"
};

var host = new WebHostBuilder()


.UseKestrel()
.UseStartup<Startup>()
.Start(urls.ToArray());

using (host)
{
Console.ReadLine();
}

La aplicación puede inicializar un nuevo host usando los valores preconfigurados de CreateDefaultBuilder
mediante un método práctico estático. Estos métodos inician el servidor sin la salida de la consola y con
WaitForShutdown esperando una interrupción (Ctrl-C/SIGINT o SIGTERM ):
Start(RequestDelegate app)
Start con RequestDelegate :

using (var host = WebHost.Start(app => app.Response.WriteAsync("Hello, World!")))


{
Console.WriteLine("Use Ctrl-C to shutdown the host...");
host.WaitForShutdown();
}

Haga una solicitud en el explorador a http://localhost:5000 para recibir la respuesta "Hello World!"
WaitForShutdown se bloquea hasta que se emite un salto ( Ctrl-C/SIGINT o SIGTERM ). La aplicación muestra el
mensaje Console.WriteLine y espera a que se pulse una tecla para salir.
Start(url de cadena, RequestDelegate app)
Start con una dirección URL y RequestDelegate :

using (var host = WebHost.Start("http://localhost:8080", app => app.Response.WriteAsync("Hello, World!")))


{
Console.WriteLine("Use Ctrl-C to shutdown the host...");
host.WaitForShutdown();
}

Produce el mismo resultado que Start(RequestDelegate app), excepto que la aplicación responde en
http://localhost:8080 .

Start(Action<IRouteBuilder> routeBuilder)
Use una instancia de IRouteBuilder (Microsoft.AspNetCore.Routing) para usar el middleware de enrutamiento:

using (var host = WebHost.Start(router => router


.MapGet("hello/{name}", (req, res, data) =>
res.WriteAsync($"Hello, {data.Values["name"]}!"))
.MapGet("buenosdias/{name}", (req, res, data) =>
res.WriteAsync($"Buenos dias, {data.Values["name"]}!"))
.MapGet("throw/{message?}", (req, res, data) =>
throw new Exception((string)data.Values["message"] ?? "Uh oh!"))
.MapGet("{greeting}/{name}", (req, res, data) =>
res.WriteAsync($"{data.Values["greeting"]}, {data.Values["name"]}!"))
.MapGet("", (req, res, data) => res.WriteAsync("Hello, World!"))))
{
Console.WriteLine("Use Ctrl-C to shutdown the host...");
host.WaitForShutdown();
}

Utilice las siguientes solicitudes de explorador con el ejemplo:

SOLICITUD RESPUESTA

http://localhost:5000/hello/Martin Hello, Martin!

http://localhost:5000/buenosdias/Catrina Buenos dias, Catrina!

http://localhost:5000/throw/ooops! Produce una excepción con la cadena "ooops!"

http://localhost:5000/throw Produce una excepción con la cadena "Uh oh!"

http://localhost:5000/Sante/Kevin Sante, Kevin!

http://localhost:5000 Hello World!

WaitForShutdown se bloquea hasta que se emite un salto (Ctrl-C/SIGINT o SIGTERM ). La aplicación muestra el
mensaje Console.WriteLine y espera a que se pulse una tecla para salir.
Start(string url, Action<IRouteBuilder> routeBuilder)
Use una dirección URL y una instancia de IRouteBuilder :
using (var host = WebHost.Start("http://localhost:8080", router => router
.MapGet("hello/{name}", (req, res, data) =>
res.WriteAsync($"Hello, {data.Values["name"]}!"))
.MapGet("buenosdias/{name}", (req, res, data) =>
res.WriteAsync($"Buenos dias, {data.Values["name"]}!"))
.MapGet("throw/{message?}", (req, res, data) =>
throw new Exception((string)data.Values["message"] ?? "Uh oh!"))
.MapGet("{greeting}/{name}", (req, res, data) =>
res.WriteAsync($"{data.Values["greeting"]}, {data.Values["name"]}!"))
.MapGet("", (req, res, data) => res.WriteAsync("Hello, World!"))))
{
Console.WriteLine("Use Ctrl-C to shut down the host...");
host.WaitForShutdown();
}

Produce el mismo resultado que Start(Action<IRouteBuilder> routeBuilder), salvo que la aplicación


responde en http://localhost:8080 .
StartWith(Action<IApplicationBuilder> app)
Proporciona un delegado para configurar IApplicationBuilder :

using (var host = WebHost.StartWith(app =>


app.Use(next =>
{
return async context =>
{
await context.Response.WriteAsync("Hello World!");
};
})))
{
Console.WriteLine("Use Ctrl-C to shut down the host...");
host.WaitForShutdown();
}

Haga una solicitud en el explorador a http://localhost:5000 para recibir la respuesta "Hello World!"
WaitForShutdown se bloquea hasta que se emite un salto ( Ctrl-C/SIGINT o SIGTERM ). La aplicación muestra el
mensaje Console.WriteLine y espera a que se pulse una tecla para salir.
StartWith(string url, Action<IApplicationBuilder> app)
Proporcione una dirección URL y un delegado para configurar IApplicationBuilder :

using (var host = WebHost.StartWith("http://localhost:8080", app =>


app.Use(next =>
{
return async context =>
{
await context.Response.WriteAsync("Hello World!");
};
})))
{
Console.WriteLine("Use Ctrl-C to shut down the host...");
host.WaitForShutdown();
}

Produce el mismo resultado que StartWith(Action<IApplicationBuilder> app), salvo que la aplicación


responde en http://localhost:8080 .

Interfaz IHostingEnvironment
La interfaz IHostingEnvironment proporciona información sobre el entorno de hospedaje web de la aplicación.
Use inserción de constructores para obtener IHostingEnvironment a fin de utilizar sus propiedades y métodos de
extensión:

public class CustomFileReader


{
private readonly IHostingEnvironment _env;

public CustomFileReader(IHostingEnvironment env)


{
_env = env;
}

public string ReadFile(string filePath)


{
var fileProvider = _env.WebRootFileProvider;
// Process the file here
}
}

Puede utilizarse un enfoque convencional para configurar la aplicación en el inicio según el entorno. Como
alternativa, inserte IHostingEnvironment en el constructor Startup para su uso en ConfigureServices :

public class Startup


{
public Startup(IHostingEnvironment env)
{
HostingEnvironment = env;
}

public IHostingEnvironment HostingEnvironment { get; }

public void ConfigureServices(IServiceCollection services)


{
if (HostingEnvironment.IsDevelopment())
{
// Development configuration
}
else
{
// Staging/Production configuration
}

var contentRootPath = HostingEnvironment.ContentRootPath;


}
}

NOTE
Además del método de extensión IsDevelopment , IHostingEnvironment ofrece los métodos IsStaging ,
IsProduction y IsEnvironment(string environmentName) . Para más información, vea Usar varios entornos.

El servicio IHostingEnvironment también se puede insertar directamente en el método Configure para


configurar la canalización de procesamiento:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
// In Development, use the developer exception page
app.UseDeveloperExceptionPage();
}
else
{
// In Staging/Production, route exceptions to /error
app.UseExceptionHandler("/error");
}

var contentRootPath = env.ContentRootPath;


}

IHostingEnvironment puede insertarse en el método Invoke al crear middleware personalizado:

public async Task Invoke(HttpContext context, IHostingEnvironment env)


{
if (env.IsDevelopment())
{
// Configure middleware for Development
}
else
{
// Configure middleware for Staging/Production
}

var contentRootPath = env.ContentRootPath;


}

Interfaz IApplicationLifetime
IApplicationLifetime permite actividades posteriores al inicio y apagado. Hay tres propiedades en la interfaz que
son tokens de cancelación usados para registrar métodos Action que definen los eventos de inicio y apagado.

TOKEN DE CANCELACIÓN SE DESENCADENA CUANDO…

ApplicationStarted El host se ha iniciado completamente.

ApplicationStopped El host está completando un apagado estable. Deben


procesarse todas las solicitudes. El apagado se bloquea hasta
que se complete este evento.

ApplicationStopping El host está realizando un apagado estable. Puede que


todavía se estén procesando las solicitudes. El apagado se
bloquea hasta que se complete este evento.
public class Startup
{
public void Configure(IApplicationBuilder app, IApplicationLifetime appLifetime)
{
appLifetime.ApplicationStarted.Register(OnStarted);
appLifetime.ApplicationStopping.Register(OnStopping);
appLifetime.ApplicationStopped.Register(OnStopped);

Console.CancelKeyPress += (sender, eventArgs) =>


{
appLifetime.StopApplication();
// Don't terminate the process immediately, wait for the Main thread to exit gracefully.
eventArgs.Cancel = true;
};
}

private void OnStarted()


{
// Perform post-startup activities here
}

private void OnStopping()


{
// Perform on-stopping activities here
}

private void OnStopped()


{
// Perform post-stopped activities here
}
}

StopApplication solicita la terminación de la aplicación. La siguiente clase usa StopApplication para cerrar de
forma estable una aplicación cuando se llama al método Shutdown de esa clase:

public class MyClass


{
private readonly IApplicationLifetime _appLifetime;

public MyClass(IApplicationLifetime appLifetime)


{
_appLifetime = appLifetime;
}

public void Shutdown()


{
_appLifetime.StopApplication();
}
}

Validación del ámbito


En ASP.NET Core 2.0 o posterior, CreateDefaultBuilder establece ServiceProviderOptions.ValidateScopes en
true si el entorno de la aplicación es desarrollo.

Cuando ValidateScopes está establecido en true , el proveedor de servicios predeterminado realiza


comprobaciones para confirmar lo siguiente:
Los servicios con ámbito no se resuelven directa o indirectamente desde el proveedor de servicios raíz.
Los servicios con ámbito no se insertan directa o indirectamente en singletons.
El proveedor de servicios raíz se crea cuando se llama a BuildServiceProvider. La vigencia del proveedor de
servicios raíz es la misma que la de la aplicación o el servidor cuando el proveedor se inicia con la aplicación, y se
elimina cuando la aplicación se cierra.
De la eliminación de los servicios con ámbito se encarga el contenedor que los creó. Si un servicio con ámbito se
crea en el contenedor raíz, su vigencia sube a la del singleton, ya que solo lo puede eliminar el contenedor raíz
cuando la aplicación o el servidor se cierran. Al validar los ámbitos de servicio, este tipo de situaciones se
detectan cuando se llama a BuildServiceProvider .
Para validar los ámbitos siempre, incluso en el entorno de producción, configure ServiceProviderOptions con
UseDefaultServiceProvider en el generador de hosts:

WebHost.CreateDefaultBuilder(args)
.UseDefaultServiceProvider((context, options) => {
options.ValidateScopes = true;
})

Solución de problemas de System.ArgumentException


Se aplica únicamente a ASP.NET Core 2.0
Se puede crear un host mediante la inserción directa de IStartup en el contenedor de inserción de dependencias
en lugar de llamar a UseStartup o Configure :

services.AddSingleton<IStartup, Startup>();

Si el host se crea de esta forma, puede producirse el error siguiente:

Unhandled Exception: System.ArgumentException: A valid non-empty application name must be provided.

Esto ocurre porque applicationName(ApplicationKey) (el ensamblado actual) es necesario para buscar
HostingStartupAttributes . Si la aplicación inserta manualmente IStartup en el contenedor de inserción de
dependencias, agregue la siguiente llamada a WebHostBuilder con el nombre de ensamblado especificado:

WebHost.CreateDefaultBuilder(args)
.UseSetting("applicationName", "<Assembly Name>")
...

O bien, agregue un Configure ficticio a WebHostBuilder , que establece applicationName ( ApplicationKey )


automáticamente:

WebHost.CreateDefaultBuilder(args)
.Configure(_ => { })
...

NOTA: Esto solo es necesario con la versión ASP.NET Core 2.0 y únicamente cuando la aplicación no llama a
UseStartup o Configure .

Para obtener más información, vea Announcements: Microsoft.Extensions.PlatformAbstractions has been


removed (comment) (Anuncios: Microsoft.Extensions.PlatformAbstractions ha sido eliminado (comentario) y el
ejemplo StartupInjection.

Recursos adicionales
Hospedaje en Windows con IIS
Hospedaje en Linux con Nginx
Hospedaje en Linux con Apache
Hospedaje en un servicio de Windows
Host genérico de .NET
04/06/2018 • 18 minutes to read • Edit Online

Por Luke Latham


Las aplicaciones .NET configuran e inician un host. El host es responsable de la administración del inicio y la
duración de la aplicación. En este tema se trata el host genérico de ASP.NET Core (HostBuilder), muy útil para
hospedar aplicaciones que no procesan solicitudes HTTP. Para la cobertura del host de web (WebHostBuilder),
consulte el tema Host web.
El objetivo del host genérico es desacoplar la canalización HTTP de la API host web para habilitar una variedad de
escenarios de host. Se trata de la mensajería, tareas en segundo plano y otras cargas de trabajo no HTTP basadas
en la ventaja de host genérico de capacidades transversales, como la configuración, la inserción de dependencias
(DI) y el registro.
El host genérico es una novedad de ASP.NET Core 2.1 y no es adecuado para escenarios de hospedaje web. Para
escenarios de hospedaje web, use el host web. El host genérico se está desarrollando para reemplazar el host web
en una próxima versión y actuar como la API del host principal tanto en escenarios que sean del tipo HTTP como
los que no.
Vea o descargue el código de ejemplo (cómo descargarlo)
Al ejecutar la aplicación de ejemplo en Visual Studio Code, use un terminal integrado o externo. No ejecute el
ejemplo en internalConsole .
Para establecer la consola en Visual Studio Code:
1. Abra el archivo .vscode/launch.json.
2. En la configuración de .NET Core Launch (console), busque la entrada console. Establezca el valor en
externalTerminal o integratedTerminal .

Introducción
La biblioteca de host genérico está disponible en el espacio de nombres de Microsoft.Extensions.Hosting y la
proporciona el paquete Microsoft.Extensions.Hosting NuGet. El paquete Microsoft.Extensions.Hosting está
incluido en el metapaquete Microsoft.AspNetCore.App.
IHostedService es el punto de entrada para la ejecución de código. Cada implementación de IHostedService se
ejecuta en el orden de registro del servicio en ConfigureServices. Se llama a StartAsync en cada IHostedService
cuando se inicia el host, y se llama a StopAsync en orden inverso del registro cuando el host se cierra de forma
estable.

Configuración de un host
IHostBuilder es el componente principal que usan las bibliotecas y aplicaciones para inicializar, compilar y ejecutar
el host:
public static async Task Main(string[] args)
{
var host = new HostBuilder()
.Build();

await host.RunAsync();
}

Configuración de host
HostBuilder se basa en los siguientes métodos para establecer los valores de configuración del host:
Generador de configuración
Configuración del método de extensión
Generador de configuración
La configuración del generador de host se crea mediante una llamada a ConfigureHostConfiguration en la
implementación de IHostBuilder. ConfigureHostConfiguration usa IConfigurationBuilder para crear
IConfiguration para el host. El generador de configuración inicializa IHostingEnvironment para su uso en el
proceso de compilación de la aplicación. Se puede llamar varias veces a ConfigureHostConfiguration con
resultados de suma. El host usa cualquier opción que establece un valor en último lugar.
hostsettings.json:

{
"environment": "Development"
}

Configuración de HostBuilder de ejemplo con ConfigureHostConfiguration :

var host = new HostBuilder()


.ConfigureHostConfiguration(configHost =>
{
configHost.AddEnvironmentVariables();
configHost.AddJsonFile("hostsettings.json", optional: true);
configHost.AddCommandLine(args);
})

NOTE
El método de extensión AddConfiguration no es capaz de analizar actualmente una sección de configuración devuelta por
GetSection (por ejemplo, .AddConfiguration(Configuration.GetSection("section")) ). El método GetSection filtra las
claves de configuración a la sección solicitada, pero deja el nombre de sección en las claves (por ejemplo,
section:environment ). El método AddConfiguration espera que las claves coincidan con las claves HostBuilder (por
ejemplo, environment ). La presencia del nombre de sección en las claves evita que los valores de la sección configuren el
host. Este problema se corregirá en una versión futura. Para obtener más información y soluciones alternativas, consulte
Passing configuration section into WebHostBuilder.UseConfiguration uses full keys (Pasar la sección de configuración a
WebHostBuilder.UseConfiguration usa claves completas).

Configuración del método de extensión


Se llama a métodos de extensión en la implementación de IHostBuilder para configurar la raíz de contenido y el
entorno.
Raíz del contenido
Esta configuración determina la ubicación en la que el host comienza a buscar archivos de contenido.
Clave: contentRoot
Tipo: cadena
Valor predeterminado: la carpeta donde se encuentra el ensamblado de la aplicación.
Establecer mediante: UseContentRoot
Variable de entorno: ASPNETCORE_CONTENTROOT
Si no existe la ruta de acceso, el host no se puede iniciar.

var host = new HostBuilder()


.UseContentRoot("c:\\<content-root>")

Entorno
Establece el entorno de la aplicación.
Clave: environment
Tipo: cadena
Valor predeterminado: producción
Establecer mediante: UseEnvironment
Variable de entorno: ASPNETCORE_ENVIRONMENT
El entorno se puede establecer en cualquier valor. Los valores definidos por el marco son Development , Staging y
Production . Los valores no distinguen mayúsculas de minúsculas. De forma predeterminada, el entorno se lee
desde la variable de entorno ASPNETCORE_ENVIRONMENT . Cuando se usa Visual Studio, las variables de entorno se
pueden establecer en el archivo launchSettings.json. Para obtener más información, consulte Uso de varios
entornos.

var host = new HostBuilder()


.UseEnvironment(EnvironmentName.Development)

ConfigureAppConfiguration
La configuración del generador de la aplicación se crea mediante una llamada a ConfigureAppConfiguration en la
implementación de IHostBuilder. ConfigureAppConfiguration usa un IConfigurationBuilder para crear un
IConfiguration para la aplicación. Se puede llamar varias veces a ConfigureAppConfiguration con resultados de
suma. La aplicación usa cualquier opción que establece un valor en último lugar. La configuración creada por
ConfigureAppConfiguration está disponible en HostBuilderContext.Configuration para las operaciones posteriores
y en Services.
Configuración de aplicación de ejemplo con ConfigureAppConfiguration :

var host = new HostBuilder()


.ConfigureAppConfiguration((hostContext, configApp) =>
{
configApp.AddEnvironmentVariables();
configApp.AddJsonFile("appsettings.json", optional: true);
configApp.AddJsonFile(
$"appsettings.{hostContext.HostingEnvironment.EnvironmentName}.json",
optional: true);
configApp.AddCommandLine(args);
})

appsettings.json:
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*"
}

appsettings.Development.json:

{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
}
}

appsettings.Production.json:

{
"Logging": {
"LogLevel": {
"Default": "Error",
"System": "Information",
"Microsoft": "Information"
}
}
}

NOTE
El método de extensión AddConfiguration no es capaz de analizar actualmente una sección de configuración devuelta por
GetSection (por ejemplo, .AddConfiguration(Configuration.GetSection("section")) ). El método GetSection filtra las
claves de configuración a la sección solicitada, pero deja el nombre de sección en las claves (por ejemplo,
section:Logging:LogLevel:Default ). El método AddConfiguration espera una coincidencia exacta con las claves de
configuración (por ejemplo, Logging:LogLevel:Default ). La presencia del nombre de sección en las claves evita que los
valores de la sección configuren la aplicación. Este problema se corregirá en una versión futura. Para obtener más
información y soluciones alternativas, consulte Passing configuration section into WebHostBuilder.UseConfiguration uses full
keys (Pasar la sección de configuración a WebHostBuilder.UseConfiguration usa claves completas).

ConfigureServices
ConfigureServices agrega los servicios al contenedor de inserción de dependencias de la aplicación. Se puede
llamar varias veces a ConfigureServices con resultados de suma.
Un servicio hospedado es una clase con lógica de tarea en segundo plano que implementa la interfaz
IHostedService. Para más información, consulte el tema Tareas en segundo plano con servicios hospedados.
La aplicación de ejemplo usa el método de extensión AddHostedService para agregar un servicio para eventos de
duración, LifetimeEventsHostedService , y una tarea en segundo plano programada, TimedHostedService , a la
aplicación:
var host = new HostBuilder()
.ConfigureServices((hostContext, services) =>
{
services.AddLogging();
services.AddHostedService<LifetimeEventsHostedService>();
services.AddHostedService<TimedHostedService>();
})

ConfigureLogging
ConfigureLogging agrega un delegado para configurar el ILoggingBuilder proporcionado. Se puede llamar varias
veces a ConfigureLogging con resultados de suma.

var host = new HostBuilder()


.ConfigureLogging((hostContext, configLogging) =>
{
configLogging.AddConsole();
configLogging.AddDebug();
})

UseConsoleLifetime
UseConsoleLifetime escucha Ctrl+C /SIGINT o SIGTERM y llama a StopApplication para iniciar el proceso de
cierre. UseConsoleLifetime desbloquea extensiones como RunAsync y WaitForShutdownAsync. ConsoleLifetime
ya está registrado previamente como la implementación de duración predeterminada. Se usa la última duración
registrada.

var host = new HostBuilder()


.UseConsoleLifetime()

Configuración del contenedor


Para permitir la conexión a otros contenedores, el host puede aceptar IServiceProviderFactory. Proporcionar un
generador no forma parte del registro de contenedor DI, sino que es un host intrínseco utilizado para crear el
contenedor DI determinado. UseServiceProviderFactory (IServiceProviderFactory<TContainerBuilder>) invalida
el generador predeterminado utilizado para crear el proveedor de servicios de la aplicación.
La configuración personalizada del contenedor está administrada por el método ConfigureContainer.
ConfigureContainer proporciona una experiencia fuertemente tipada para configurar el contenedor sobre la API
de host subyacente. Se puede llamar varias veces a ConfigureContainer con resultados de suma.
Crear un contenedor de servicios de la aplicación:

namespace GenericHostSample
{
internal class ServiceContainer
{
}
}

Proporcionar un generador de contenedor de servicio:


using System;
using Microsoft.Extensions.DependencyInjection;

namespace GenericHostSample
{
internal class ServiceContainerFactory : IServiceProviderFactory<MyContainer>
{
public ServiceContainer CreateBuilder(IServiceCollection services)
{
return new ServiceContainer();
}

public IServiceProvider CreateServiceProvider(ServiceContainer containerBuilder)


{
throw new NotImplementedException();
}
}
}

Usar el generador y configurar el contenedor de servicio personalizado de la aplicación:

var host = new HostBuilder()


.UseServiceProviderFactory<ServiceContainer>(new SerivceContainerFactory())
.ConfigureContainer<ServiceContainer>((hostContext, container) =>
{
})

Extensibilidad
La extensibilidad de host se realiza con métodos de extensión en IHostBuilder . El ejemplo siguiente muestra
cómo un método de extensión extiende una implementación de IHostBuilder con RabbitMQ. El método de
extensión (en otra parte de la aplicación) registra un IHostedService de RabbitMQ:

// UseRabbitMq is an extension method that sets up RabbitMQ to handle incoming


// messages.
var host = new HostBuilder()
.UseRabbitMq<MyMessageHandler>()
.Build();

await host.StartAsync();

Administración del host


La implementación de IHost es la responsable de iniciar y detener las implementaciones de IHostedService que
están registradas en el contenedor de servicios.
Run
Run inicia la aplicación y bloquea el subproceso que realiza la llamada hasta que se cierre el host:
public class Program
{
public void Main(string[] args)
{
var host = new HostBuilder()
.Build();

host.Run();
}
}

RunAsync
RunAsync inicia la aplicación y devuelve Task , que se completa cuando se desencadena el token de cancelación o
el cierre:

public class Program


{
public static async Task Main(string[] args)
{
var host = new HostBuilder()
.Build();

await host.RunAsync();
}
}

RunConsoleAsync
RunConsoleAsync habilita la compatibilidad de la consola, compila e inicia el host y espera a que se cierre Ctrl+C
/SIGINT o SIGTERM.

public class Program


{
public static async Task Main(string[] args)
{
var hostBuilder = new HostBuilder();

await hostBuilder.RunConsoleAsync();
}
}

Start y StopAsync
Start inicia el host de forma sincrónica.
StopAsync(TimeSpan) intenta detener el host en el tiempo de espera proporcionado.
public class Program
{
public static async Task Main(string[] args)
{
var host = new HostBuilder()
.Build();

using (host)
{
host.Start();

await host.StopAsync(TimeSpan.FromSeconds(5));
}
}
}

StartAsync y StopAsync
StartAsync inicia la aplicación.
StopAsync detiene la aplicación.

public class Program


{
public static async Task Main(string[] args)
{
var host = new HostBuilder()
.Build();

using (host)
{
await host.StartAsync();

await host.StopAsync();
}
}
}

WaitForShutdown
WaitForShutdown se desencadena mediante IHostLifetime, como ConsoleLifetime (escucha Ctrl+C /SIGINT o
SIGTERM ). WaitForShutdown llama a StopAsync.

public class Program


{
public void Main(string[] args)
{
var host = new HostBuilder()
.Build();

using (host)
{
host.Start();

host.WaitForShutdown();
}
}
}

WaitForShutdownAsync
WaitForShutdownAsync devuelve Task , que se completa cuando se desencadena el cierre a través del token
determinado y llama a StopAsync.
public class Program
{
public static async Task Main(string[] args)
{
var host = new HostBuilder()
.Build();

using (host)
{
await host.StartAsync();

await host.WaitForShutdownAsync();
}

}
}

Control externo
El control externo del host se puede lograr mediante métodos a los que se pueda llamar de forma externa:

public class Program


{
private IHost _host;

public Program()
{
_host = new HostBuilder()
.Build();
}

public async Task StartAsync()


{
_host.StartAsync();
}

public async Task StopAsync()


{
using (_host)
{
await _host.StopAsync(TimeSpan.FromSeconds(5));
}
}
}

IHostLifetime.WaitForStartAsync se llama al inicio de StartAsync, que espera hasta que se complete antes de
continuar. Esto se puede usar para retrasar el inicio hasta que lo indique un evento externo.

Interfaz IHostingEnvironment
IHostingEnvironment proporciona información sobre el entorno de hospedaje de la aplicación. Use inserción de
constructores para obtener IHostingEnvironment a fin de utilizar sus propiedades y métodos de extensión:
public class MyClass
{
private readonly IHostingEnvironment _env;

public MyClass(IHostingEnvironment env)


{
_env = env;
}

public void DoSomething()


{
var environmentName = _env.EnvironmentName;
}
}

Para obtener más información, consulte Uso de varios entornos.

Interfaz IApplicationLifetime
IApplicationLifetime permite actividades posteriores al inicio y cierre, incluidas las solicitudes de cierre estable.
Hay tres propiedades en la interfaz que son tokens de cancelación usados para registrar métodos Action que
definen los eventos de inicio y apagado.

TOKEN DE CANCELACIÓN SE DESENCADENA CUANDO…

ApplicationStarted El host se ha iniciado completamente.

ApplicationStopped El host está completando un apagado estable. Deben


procesarse todas las solicitudes. El apagado se bloquea hasta
que se complete este evento.

ApplicationStopping El host está realizando un apagado estable. Puede que


todavía se estén procesando las solicitudes. El apagado se
bloquea hasta que se complete este evento.

Inserción de constructor del servicio IApplicationLifetime en cualquier clase. La aplicación de ejemplo utiliza la
inserción de constructor en una clase LifetimeEventsHostedService (una implementación de IHostedService ) para
registrar los eventos.
LifetimeEventsHostedService.cs:
internal class LifetimeEventsHostedService : IHostedService
{
private readonly ILogger _logger;
private readonly IApplicationLifetime _appLifetime;

public LifetimeEventsHostedService(
ILogger<LifetimeEventsHostedService> logger, IApplicationLifetime appLifetime)
{
_logger = logger;
_appLifetime = appLifetime;
}

public Task StartAsync(CancellationToken cancellationToken)


{
_appLifetime.ApplicationStarted.Register(OnStarted);
_appLifetime.ApplicationStopping.Register(OnStopping);
_appLifetime.ApplicationStopped.Register(OnStopped);

return Task.CompletedTask;
}

public Task StopAsync(CancellationToken cancellationToken)


{
return Task.CompletedTask;
}

private void OnStarted()


{
_logger.LogInformation("OnStarted has been called.");

// Perform post-startup activities here


}

private void OnStopping()


{
_logger.LogInformation("OnStopping has been called.");

// Perform on-stopping activities here


}

private void OnStopped()


{
_logger.LogInformation("OnStopped has been called.");

// Perform post-stopped activities here


}
}

StopApplication solicita la terminación de la aplicación. La siguiente clase usa StopApplication para cerrar de
forma estable una aplicación cuando se llama al método Shutdown de esa clase:
public class MyClass
{
private readonly IApplicationLifetime _appLifetime;

public MyClass(IApplicationLifetime appLifetime)


{
_appLifetime = appLifetime;
}

public void Shutdown()


{
_appLifetime.StopApplication();
}
}

Recursos adicionales
Tareas en segundo plano con servicios hospedados
Ejemplos de hospedaje de repositorios en GitHub
Tareas en segundo plano con servicios hospedados
en ASP.NET Core
31/05/2018 • 7 minutes to read • Edit Online

Por Luke Latham


En ASP.NET Core, las tareas en segundo plano se pueden implementar como servicios hospedados. Un servicio
hospedado es una clase con lógica de tarea en segundo plano que implementa la interfaz IHostedService. En este
tema se incluyen tres ejemplos de servicio hospedado:
Una tarea en segundo plano que se ejecuta según un temporizador.
Un servicio hospedado que activa un servicio con ámbito. El servicio con ámbito puede usar la inserción de
dependencias.
Tareas en segundo plano en cola que se ejecutan en secuencia.
Vea o descargue el código de ejemplo (cómo descargarlo)
La aplicación de ejemplo se ofrece en dos versiones:
Host de web: el host de web resulta útil para hospedar aplicaciones web. El código de ejemplo que se muestra
en este tema corresponde a la versión de host de web del ejemplo. Para más información, vea el sitio web Host
de web.
Host genérico: el host genérico es nuevo en ASP.NET Core 2.1. Para más información, vea el sitio web Host
genérico.

Interfaz IHostedService
Los servicios hospedados implementan la interfaz IHostedService. Esta interfaz define dos métodos para los
objetos administrados por el host:
StartAsync(CancellationToken): se llama después de que el servidor se haya iniciado y
IApplicationLifetime.ApplicationStarted se haya activado. StartAsync contiene la lógica para iniciar la tarea
en segundo plano.
StopAsync(CancellationToken): se activa cuando el host está realizando un cierre estable. StopAsync
contiene la lógica para finalizar la tarea en segundo plano y desechar los recursos no administrados. Si la
aplicación se cierra inesperadamente (por ejemplo, porque se produzca un error en el proceso de la
aplicación), puede que no sea posible llamar a StopAsync .
El servicio hospedado es un singleton que se activa una vez en el inicio de la aplicación y se cierra de manera
estable cuando dicha aplicación se cierra. Si IDisposable está implementada, se pueden desechar recursos cuando
se deseche el contenedor de servicios. Si se produce un error durante la ejecución de una tarea en segundo plano,
hay que llamar a Dispose , aun cuando no se haya llamado a StopAsync .

Tareas en segundo plano temporizadas


Una tarea en segundo plano temporizada hace uso de la clase System.Threading.Timer. El temporizador activa el
método DoWork de la tarea. El temporizador está deshabilitado en StopAsync y se desecha cuando el contenedor
de servicios se elimina en Dispose :
internal class TimedHostedService : IHostedService, IDisposable
{
private readonly ILogger _logger;
private Timer _timer;

public TimedHostedService(ILogger<TimedHostedService> logger)


{
_logger = logger;
}

public Task StartAsync(CancellationToken cancellationToken)


{
_logger.LogInformation("Timed Background Service is starting.");

_timer = new Timer(DoWork, null, TimeSpan.Zero,


TimeSpan.FromSeconds(5));

return Task.CompletedTask;
}

private void DoWork(object state)


{
_logger.LogInformation("Timed Background Service is working.");
}

public Task StopAsync(CancellationToken cancellationToken)


{
_logger.LogInformation("Timed Background Service is stopping.");

_timer?.Change(Timeout.Infinite, 0);

return Task.CompletedTask;
}

public void Dispose()


{
_timer?.Dispose();
}
}

El servicio se registra en Startup.ConfigureServices :

services.AddHostedService<TimedHostedService>();

Consumir un servicio con ámbito en una tarea en segundo plano


Para usar servicios con ámbito en un IHostedService , cree un ámbito. No se crean ámbitos de forma
predeterminada para los servicios hospedados.
El servicio de tareas en segundo plano con ámbito contiene la lógica de la tarea en segundo plano. En el siguiente
ejemplo, ILogger se inserta en el servicio:
internal interface IScopedProcessingService
{
void DoWork();
}

internal class ScopedProcessingService : IScopedProcessingService


{
private readonly ILogger _logger;

public ScopedProcessingService(ILogger<ScopedProcessingService> logger)


{
_logger = logger;
}

public void DoWork()


{
_logger.LogInformation("Scoped Processing Service is working.");
}
}

El servicio hospedado crea un ámbito con objeto de resolver el servicio de tareas en segundo plano con ámbito
para llamar a su método DoWork :
internal class ConsumeScopedServiceHostedService : IHostedService
{
private readonly ILogger _logger;

public ConsumeScopedServiceHostedService(IServiceProvider services,


ILogger<ConsumeScopedServiceHostedService> logger)
{
Services = services;
_logger = logger;
}

public IServiceProvider Services { get; }

public Task StartAsync(CancellationToken cancellationToken)


{
_logger.LogInformation(
"Consume Scoped Service Hosted Service is starting.");

DoWork();

return Task.CompletedTask;
}

private void DoWork()


{
_logger.LogInformation(
"Consume Scoped Service Hosted Service is working.");

using (var scope = Services.CreateScope())


{
var scopedProcessingService =
scope.ServiceProvider
.GetRequiredService<IScopedProcessingService>();

scopedProcessingService.DoWork();
}
}

public Task StopAsync(CancellationToken cancellationToken)


{
_logger.LogInformation(
"Consume Scoped Service Hosted Service is stopping.");

return Task.CompletedTask;
}
}

Los servicios se registran en Startup.ConfigureServices :

services.AddHostedService<ConsumeScopedServiceHostedService>();
services.AddScoped<IScopedProcessingService, ScopedProcessingService>();

Tareas en segundo plano en cola


Las colas de tareas en segundo plano se basan en QueueBackgroundWorkItem de .NET 4.x (está previsto que se
integre en ASP.NET Core 2.2):
public interface IBackgroundTaskQueue
{
void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem);

Task<Func<CancellationToken, Task>> DequeueAsync(


CancellationToken cancellationToken);
}

public class BackgroundTaskQueue : IBackgroundTaskQueue


{
private ConcurrentQueue<Func<CancellationToken, Task>> _workItems =
new ConcurrentQueue<Func<CancellationToken, Task>>();
private SemaphoreSlim _signal = new SemaphoreSlim(0);

public void QueueBackgroundWorkItem(


Func<CancellationToken, Task> workItem)
{
if (workItem == null)
{
throw new ArgumentNullException(nameof(workItem));
}

_workItems.Enqueue(workItem);
_signal.Release();
}

public async Task<Func<CancellationToken, Task>> DequeueAsync(


CancellationToken cancellationToken)
{
await _signal.WaitAsync(cancellationToken);
_workItems.TryDequeue(out var workItem);

return workItem;
}
}

En QueueHostedService , las tareas en segundo plano ( workItem ) que están en cola se quitan de ella y se ejecutan:
public class QueuedHostedService : IHostedService
{
private CancellationTokenSource _shutdown =
new CancellationTokenSource();
private Task _backgroundTask;
private readonly ILogger _logger;

public QueuedHostedService(IBackgroundTaskQueue taskQueue,


ILoggerFactory loggerFactory)
{
TaskQueue = taskQueue;
_logger = loggerFactory.CreateLogger<QueuedHostedService>();
}

public IBackgroundTaskQueue TaskQueue { get; }

public Task StartAsync(CancellationToken cancellationToken)


{
_logger.LogInformation("Queued Hosted Service is starting.");

_backgroundTask = Task.Run(BackgroundProceessing);

return Task.CompletedTask;
}

private async Task BackgroundProceessing()


{
while (!_shutdown.IsCancellationRequested)
{
var workItem =
await TaskQueue.DequeueAsync(_shutdown.Token);

try
{
await workItem(_shutdown.Token);
}
catch (Exception ex)
{
_logger.LogError(ex,
$"Error occurred executing {nameof(workItem)}.");
}
}
}

public Task StopAsync(CancellationToken cancellationToken)


{
_logger.LogInformation("Queued Hosted Service is stopping.");

_shutdown.Cancel();

return Task.WhenAny(_backgroundTask,
Task.Delay(Timeout.Infinite, cancellationToken));
}
}

Los servicios se registran en Startup.ConfigureServices :

services.AddHostedService<QueuedHostedService>();
services.AddSingleton<IBackgroundTaskQueue, BackgroundTaskQueue>();

En la clase de modelo de página de índice, IBackgroundTaskQueue se inserta en el constructor y se asigna a Queue :


public IndexModel(IBackgroundTaskQueue queue,
IApplicationLifetime appLifetime,
ILogger<IndexModel> logger)
{
Queue = queue;
_appLifetime = appLifetime;
_logger = logger;
}

public IBackgroundTaskQueue Queue { get; }

Cuando se hace clic en el botón Agregar tarea en la página de índice, se ejecuta el método OnPostAddTask . Se
llama a QueueBackgroundWorkItem para poner en cola el elemento de trabajo:

public IActionResult OnPostAddTask()


{
Queue.QueueBackgroundWorkItem(async token =>
{
var guid = Guid.NewGuid().ToString();

for (int delayLoop = 0; delayLoop < 3; delayLoop++)


{
_logger.LogInformation(
$"Queued Background Task {guid} is running. {delayLoop}/3");
await Task.Delay(TimeSpan.FromSeconds(5), token);
}

_logger.LogInformation(
$"Queued Background Task {guid} is complete. 3/3");
});

return RedirectToPage();
}

Recursos adicionales
Implementar tareas en segundo plano en microservicios con IHostedService y la clase BackgroundService
System.Threading.Timer
Estado de sesión y aplicación en ASP.NET Core
19/06/2018 • 26 minutes to read • Edit Online

Por Rick Anderson, Steve Smith y Diana LaRose


HTTP es un protocolo sin estado. Un servidor web trata de manera independiente cada solicitud HTTP y no
conserva los valores de usuario de las solicitudes anteriores. En este artículo se describen diferentes maneras de
mantener el estado sesión y aplicación entre las solicitudes.

Estado de sesión
El estado de sesión es una característica de ASP.NET Core que se puede usar para guardar y almacenar datos de
usuario mientras el usuario explora la aplicación web. El estado de sesión, que está compuesto por un diccionario
o tabla hash en el servidor, conserva los datos de todas las solicitudes desde un explorador. Los datos de sesión
se guardan en una copia de seguridad en una memoria caché.
Para mantener el estado de sesión, ASP.NET Core proporciona al cliente una cookie que contiene el identificador
de sesión, que se envía al servidor con cada solicitud. El servidor utiliza el identificador de sesión para capturar
los datos de sesión. Dado que la cookie de sesión es específica del explorador, no es posible compartir las
sesiones entre los exploradores. Las cookies de sesión se eliminan cuando finaliza la sesión del explorador. Si se
recibe una cookie de una sesión que ha expirado, se crea una nueva sesión que usa la misma cookie de sesión.
El servidor conserva una sesión durante un tiempo limitado después de la última solicitud. Se puede especificar
un tiempo de espera de sesión o usar el valor predeterminado de 20 minutos. El estado de sesión es ideal para
almacenar datos de usuario que son específicos de una sesión determinada, pero que no necesitan conservarse
de forma permanente. Los datos se eliminan de la memoria auxiliar cuando se llama a Session.Clear o cuando
la sesión expira en el almacén de datos. El servidor no sabe cuándo se cierra el explorador o cuándo se elimina la
cookie de sesión.

WARNING
No almacene datos confidenciales en la sesión, dado que el cliente podría no cerrar el explorador ni borrar la cookie de
sesión (además, algunos exploradores mantienen las cookies de sesión activas en distintas ventanas). También es posible
que una sesión no esté restringida a un único usuario y que el siguiente usuario continúe con la misma sesión.

El proveedor de sesión en memoria almacena datos de la sesión en el servidor local. Si tiene previsto ejecutar la
aplicación web en una granja de servidores, debe usar sesiones permanentes para asociar cada sesión a un
servidor específico. El valor predeterminado de la plataforma Windows Azure Web Sites es usar sesiones
permanentes (enrutamiento de solicitud de aplicación o ARR ). Pero las sesiones permanentes pueden afectar a la
escalabilidad y complicar la actualización de las aplicaciones web. Una mejor opción consiste en usar las
memorias caché distribuidas de Redis o SQL Server, que no requieren sesiones permanentes. Para más
información, vea Working with a Distributed Cache (Trabajar con una memoria caché distribuida). Para obtener
más información sobre cómo configurar los proveedores de servicios, consulte Configuración de sesión más
adelante en este artículo.

TempData
ASP.NET Core MVC expone la propiedad TempData en un controlador. Esta propiedad almacena datos hasta que
se leen. Los métodos Keep y Peek se pueden usar para examinar los datos sin que se eliminen. TempData es
particularmente útil para el redireccionamiento cuando se necesitan los datos de más de una única solicitud. Los
proveedores de TempData implementan TempData mediante, por ejemplo, cookies o estado de sesión.
Proveedores de TempData
ASP.NET Core 2.x
ASP.NET Core 1.x
En ASP.NET Core 2.0 y versiones posteriores, el proveedor TempData basado en cookies se utiliza de forma
predeterminada para almacenar TempData en cookies.
Los datos de cookie se cifran mediante IDataProtector, codificado con Base64UrlTextEncoder, y después se
fragmentan. Como la cookie está fragmentada, no se aplica el límite de tamaño único de cookie que se encuentra
en ASP.NET Core 1.x. Los datos de cookie no se comprimen porque la compresión de datos cifrados puede
provocar problemas de seguridad como los ataques CRIME y BREACH. Para obtener más información sobre el
proveedor TempData basado en cookies, consulte CookieTempDataProvider.
Elegir un proveedor TempData
Elegir un proveedor TempData implica una serie de consideraciones:
1. ¿La aplicación ya usa el estado de sesión para otros fines? Si es así, la utilización del proveedor TempData de
estado de sesión no tiene costo adicional para la aplicación (excepto en el tamaño de los datos).
2. ¿La aplicación usa TempData con moderación, solo para cantidades relativamente pequeñas de datos (hasta
500 bytes)? Si es así, el proveedor TempData de cookies agregará un pequeño costo a cada solicitud que
transporta TempData. De lo contrario, el proveedor TempData de estado de sesión puede ser beneficioso para
evitar que una gran cantidad de datos hagan un recorrido de ida y vuelta en cada solicitud hasta que se
consuma TempData.
3. ¿La aplicación se ejecuta en una granja de servidores web (varios servidores)? En caso afirmativo, no es
necesario realizar ninguna otra configuración para usar el proveedor TempData de cookies.

NOTE
La mayoría de los clientes de web (por ejemplo, los exploradores web) aplican límites en el tamaño máximo de cada cookie,
el número total de cookies o ambos. Por lo tanto, cuando use el proveedor TempData de cookies, compruebe que la
aplicación no supera esos límites. Valore el tamaño total de los datos, teniendo en cuenta las sobrecargas de cifrado y
fragmentación.

Configurar el proveedor TempData


ASP.NET Core 2.x
ASP.NET Core 1.x
El proveedor TempData basado en cookies está habilitado de forma predeterminada. El siguiente código de clase
Startup configura el proveedor TempData basado en sesión:

public void ConfigureServices(IServiceCollection services)


{
services.AddMvc()
.AddSessionStateTempDataProvider();

services.AddSession();
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)


{
app.UseSession();
app.UseMvcWithDefaultRoute();
}
El orden es fundamental para los componentes de middleware. En el ejemplo anterior, se produce una excepción
de tipo InvalidOperationException cuando UseSession se invoca después de UseMvcWithDefaultRoute . Vea
Ordenación de middleware para obtener más detalles.

IMPORTANT
Si el destino es .NET Framework y usa el proveedor basado en sesión, agregue el paquete NuGet
Microsoft.AspNetCore.Session a su proyecto.

Cadenas de consulta
Puede pasar una cantidad limitada de datos de una solicitud a otra si agrega los datos a la cadena de consulta de
la solicitud nueva. Esto es útil para capturar el estado de una forma persistente que permita que los vínculos con
estado insertado se compartan a través del correo electrónico o las redes sociales. Pero, por esta razón, nunca
deben usarse las cadenas de consulta para datos confidenciales. Además de que se pueden compartir con
facilidad, la inclusión de los datos en cadenas de consulta puede propiciar ataques de falsificación de solicitud
entre sitios (CSRF ), cuya intención es engañar a los usuarios para que visiten sitios malintencionados mientras
están autenticados. A continuación, los atacantes pueden robar los datos de usuario de la aplicación o realizar
acciones malintencionadas en nombre del usuario. Cualquier estado de sesión o aplicación conservado debe
protegerse contra los ataques CSRF. Para más información sobre los ataques CSRF, vea Prevent Cross-Site
Request Forgery (XSRF/CSRF ) Attacks (Evitar los ataques de falsificación de solicitud entre sitios [XSRF/CSRF ]).

Exposición de datos y campos ocultos


Los datos pueden guardarse en campos ocultos de formulario e incluirse de nuevo en la siguiente solicitud. Esto
es habitual en los formularios de varias páginas. Pero, dado que el cliente puede llegar a alterar los datos, el
servidor debe siempre revalidarlos.

Cookies
Las cookies proporcionan una manera de almacenar datos específicos del usuario en aplicaciones web. Dado que
las cookies se envían con cada solicitud, su tamaño debe reducirse al mínimo. Lo ideal es que en cada cookie se
almacene un solo identificador y que los datos reales se almacenen en el servidor. La mayoría de los
exploradores restringen el tamaño de las cookies a 4096 bytes. Además, solo hay disponible un número limitado
de cookies para cada dominio.
Como las cookies están expuestas a alteraciones, deben validarse en el servidor. Aunque la duración de la cookie
en un cliente está sujeta a un plazo de expiración y a la intervención del usuario, generalmente son la forma más
duradera de persistencia de datos en el cliente.
Las cookies suelen utilizarse para personalizar el contenido ofrecido a un usuario conocido. Puesto que en la
mayoría de los casos el usuario tan solo se identifica y no se autentica, normalmente se puede proteger una
cookie si se almacena el nombre de usuario, el nombre de cuenta o un identificador de usuario único (por
ejemplo, un GUID ) en la cookie. A continuación, se puede usar la cookie para tener acceso a la infraestructura de
personalización de usuarios de un sitio.

HttpContext.Items
La colección Items es un buen lugar para almacenar los datos que solo son necesarios para el procesamiento de
una solicitud determinada. El contenido de la colección se descarta después de cada solicitud. Se aconseja utilizar
la colección Items como forma de comunicación entre los componentes o el middleware cuando operan en
distintos puntos en el tiempo durante una solicitud y no pueden pasarse parámetros de forma directa. Para más
información, vea Trabajar con HttpContext.Items, más adelante en este artículo.
instancias y claves
El almacenamiento en caché es una manera eficaz de almacenar y recuperar datos. Puede controlar la duración
de los elementos almacenados en caché en función del tiempo y otras consideraciones. Obtenga más
información sobre cómo almacenar en caché.

Trabajar con el estado de sesión


Configuración de sesión
El paquete Microsoft.AspNetCore.Session proporciona middleware para administrar el estado de sesión. Para
habilitar el middleware de sesión, Startup debe contener:
Cualquiera de las cachés de memoria IDistributedCache. La implementación de IDistributedCache se usa
como una memoria auxiliar para la sesión.
Una llamada a AddSession, para lo cual se requiere el paquete NuGet "Microsoft.AspNetCore.Session".
Una llamada a UseSession.
El código siguiente muestra cómo configurar el proveedor de sesión en memoria.
ASP.NET Core 2.x
ASP.NET Core 1.x

using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using System;

public class Startup


{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();

// Adds a default in-memory implementation of IDistributedCache.


services.AddDistributedMemoryCache();

services.AddSession(options =>
{
// Set a short timeout for easy testing.
options.IdleTimeout = TimeSpan.FromSeconds(10);
options.Cookie.HttpOnly = true;
});
}

public void Configure(IApplicationBuilder app)


{
app.UseSession();
app.UseMvcWithDefaultRoute();
}
}

Puede hacer referencia a la sesión de HttpContext una vez que se ha instalado y configurado.
Si se intenta acceder a Session antes de haber llamado a UseSession , se produce la excepción
InvalidOperationException: Session has not been configured for this application or request .

Si intenta crear una nueva Session (es decir, no se ha creado ninguna cookie de sesión) después de que ya haya
empezado a escribir en el flujo Response , se produce la excepción
InvalidOperationException: The session cannot be established after the response has started . La excepción
puede encontrarse en el registro de servidor web; no se mostrará en el explorador.
Carga de sesión de forma asincrónica
El proveedor de sesión predeterminado de ASP.NET Core carga asincrónicamente el registro de sesión desde el
almacén subyacente IDistributedCache solo si el método ISession.LoadAsync se llama explícitamente antes que
los métodos TryGetValue , Set o Remove . Si primero no se llama a LoadAsync , el registro de sesión subyacente
se carga de forma sincrónica, lo que podría tener consecuencias sobre la capacidad de la aplicación para
escalarse.
Para que las aplicaciones impongan este patrón, ajuste las implementaciones de DistributedSessionStore y
DistributedSession con versiones que produzcan una excepción si el método LoadAsync no se llama antes que
TryGetValue , Set o Remove . Registre las versiones ajustadas en el contenedor de servicios.

Detalles de la implementación
La sesión utiliza una cookie para realizar el seguimiento de las solicitudes emitidas por un solo explorador e
identificarlas. De manera predeterminada, esta cookie se denomina ".AspNet.Session" y utiliza una ruta de acceso
de "/". Dado que el valor predeterminado de la cookie no especifica un dominio, no estará disponible para el
script de cliente en la página (porque CookieHttpOnly tiene como valor predeterminado true ).
Para reemplazar los valores predeterminados de la sesión, use SessionOptions :
ASP.NET Core 2.x
ASP.NET Core 1.x

public void ConfigureServices(IServiceCollection services)


{
services.AddMvc();

// Adds a default in-memory implementation of IDistributedCache.


services.AddDistributedMemoryCache();

services.AddSession(options =>
{
options.Cookie.Name = ".AdventureWorks.Session";
options.IdleTimeout = TimeSpan.FromSeconds(10);
});
}

El servidor usa la propiedad IdleTimeout para determinar el tiempo que una sesión puede estar inactiva antes
de que se abandone su contenido. Esta propiedad es independiente de la expiración de la cookie. Cada solicitud
que se pasa a través del middleware de sesión (leídas o escritas) restablece el tiempo de espera.
Dado que Session no realiza bloqueo, si dos solicitudes intentan modificar el contenido de la sesión, la última
de ellas reemplaza a la primera. Session se implementa como una sesión coherente, lo que significa que todo el
contenido se almacena junto. Dos solicitudes que estén modificando distintas partes de la sesión (claves
diferentes) aún podrían verse afectadas entre sí.
Establecer y obtener valores de Session
El acceso a Session se realiza desde una vista o página de Razor con Context.Session :

@using Microsoft.AspNetCore.Http
Session Value = @Context.Session.GetString("_Name")

El acceso a Session se realiza desde un controlador o clase PageModel con HttpContext.Session : Esta propiedad
es una implementación de ISession.
En el ejemplo siguiente se muestra cómo establecer y obtener un valor int y una cadena:
public class HomeController : Controller
{
const string SessionKeyName = "_Name";
const string SessionKeyYearsMember = "_YearsMember";
const string SessionKeyDate = "_Date";

public IActionResult Index()


{
// Requires using Microsoft.AspNetCore.Http;
HttpContext.Session.SetString(SessionKeyName, "Rick");
HttpContext.Session.SetInt32(SessionKeyYearsMember, 3);
return RedirectToAction("SessionNameYears");
}
public IActionResult SessionNameYears()
{
var name = HttpContext.Session.GetString(SessionKeyName);
var yearsMember = HttpContext.Session.GetInt32(SessionKeyYearsMember);

return Content($"Name: \"{name}\", Membership years: \"{yearsMember}\"");


}
}

Si agrega los siguientes métodos de extensión, puede establecer y obtener objetos serializables en sesión:

using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;

public static class SessionExtensions


{
public static void Set<T>(this ISession session, string key, T value)
{
session.SetString(key, JsonConvert.SerializeObject(value));
}

public static T Get<T>(this ISession session,string key)


{
var value = session.GetString(key);
return value == null ? default(T) :
JsonConvert.DeserializeObject<T>(value);
}
}

En el ejemplo siguiente se muestra cómo establecer y obtener un objeto serializable:

public IActionResult SetDate()


{
// Requires you add the Set extension method mentioned in the article.
HttpContext.Session.Set<DateTime>(SessionKeyDate, DateTime.Now);
return RedirectToAction("GetDate");
}

public IActionResult GetDate()


{
// Requires you add the Get extension method mentioned in the article.
var date = HttpContext.Session.Get<DateTime>(SessionKeyDate);
var sessionTime = date.TimeOfDay.ToString();
var currentTime = DateTime.Now.TimeOfDay.ToString();

return Content($"Current time: {currentTime} - "


+ $"session time: {sessionTime}");
}

Trabajar con HttpContext.Items


Trabajar con HttpContext.Items
La abstracción HttpContext proporciona compatibilidad para una colección de diccionarios de tipo
IDictionary<object, object> , llamada Items . Esta colección está disponible desde el inicio de una solicitud
HttpRequest y se descarta al final de cada solicitud. Para acceder a ella, se puede asignar un valor a una entrada
con clave, o solicitar el valor de una clave determinada.
En el ejemplo siguiente, Middleware agrega isVerified a la colección Items .

app.Use(async (context, next) =>


{
// perform some verification
context.Items["isVerified"] = true;
await next.Invoke();
});

Más adelante en la canalización, otro middleware podría acceder a ella:

app.Run(async (context) =>


{
await context.Response.WriteAsync("Verified request? " +
context.Items["isVerified"]);
});

Si el middleware se va a usar en una única aplicación, se aceptan claves string . Pero el middleware que se
compartirá entre aplicaciones debe usar claves de objeto únicas para evitar cualquier posibilidad de colisión
entre claves. Si está desarrollando middleware que debe trabajar en varias aplicaciones, utilice una clave de
objeto única definida en la clase de middleware, tal y como se muestra a continuación:

public class SampleMiddleware


{
public static readonly object SampleKey = new Object();

public async Task Invoke(HttpContext httpContext)


{
httpContext.Items[SampleKey] = "some value";
// additional code omitted
}
}

Otro código puede tener acceso al valor almacenado en HttpContext.Items con la clave que expone la clase de
middleware:

public class HomeController : Controller


{
public IActionResult Index()
{
string value = HttpContext.Items[SampleMiddleware.SampleKey];
}
}

Este enfoque también tiene la ventaja de eliminar la repetición de "cadenas mágicas" en varios lugares en el
código.

Datos de estado de aplicación


Use inserción de dependencias para que los datos estén disponibles para todos los usuarios:
1. Defina un servicio que contenga los datos (por ejemplo, una clase denominada MyAppData ).

public class MyAppData


{
// Declare properties/methods/etc.
}

2. Agregue la clase de servicio a ConfigureServices (por ejemplo services.AddSingleton<MyAppData>(); ).


3. Utilice la clase de servicio de datos en cada controlador:

public class MyController : Controller


{
public MyController(MyAppData myService)
{
// Do something with the service (read some data from it,
// store it in a private field/property, etc.)
}
}

Errores comunes al trabajar con sesión


"No se puede resolver el servicio para el tipo 'Microsoft.Extensions.Caching.Distributed.IDistributedCache'
al intentar activar 'Microsoft.AspNetCore.Session.DistributedSessionStore'".
Esto puede deberse a que no se ha configurado al menos una implementación IDistributedCache . Para
más información, vea Working with a Distributed Cache (Trabajar con una memoria caché distribuida) e In
memory caching (Almacenamiento en memoria caché).
En caso de que el middleware de sesión no logre conservar una sesión (por ejemplo, si la base de datos no
está disponible), registra la excepción y la acepta. La solicitud continuará entonces de modo normal, lo que
provoca un comportamiento muy imprevisible.
Este es un ejemplo típico:
Alguien almacena una cesta de la compra en la sesión. El usuario agrega un elemento, pero se produce un error
en la confirmación. La aplicación no se percata del error y notifica que "el elemento se ha agregado", lo cual no es
cierto.
La manera recomendada para comprobar estos errores es llamar a await feature.Session.CommitAsync(); desde
el código de la aplicación cuando se haya terminado de escribir en la sesión. A continuación, puede hacer lo que
quiera con el error. El funcionamiento es el mismo que al llamar a LoadAsync .
Recursos adicionales
ASP.NET Core 1.x: ejemplo de código que se emplea en este documento
ASP.NET Core 2.x: ejemplo de código que se emplea en este documento
Implementaciones de servidores web en ASP.NET
Core
21/06/2018 • 10 minutes to read • Edit Online

Por Tom Dykstra, Steve Smith, Stephen Halter y Chris Ross


Una aplicación ASP.NET Core se ejecuta con una implementación de servidor HTTP en proceso. La
implementación del servidor realiza escuchas de solicitudes HTTP y las muestra en la aplicación como conjuntos
de características de solicitud compuestos en un HttpContext.
ASP.NET Core incluye dos implementaciones de servidor:
Kestrel es el servidor HTTP multiplataforma predeterminado de ASP.NET Core.
HTTP.sys es un servidor HTTP solo para Windows que se basa en el controlador de kernel de HTTP.Sys y
HTTP Server API. (HTTP.sys se denomina WebListener en ASP.NET Core 1.x).

Kestrel
Kestrel es el servidor web predeterminado que se incluye en las plantillas de proyecto de ASP.NET Core.
ASP.NET Core 2.x
ASP.NET Core 1.x
Se puede usar Kestrel por sí solo o con un servidor proxy inverso, como IIS, Nginx o Apache. Un servidor proxy
inverso recibe las solicitudes HTTP de Internet y las reenvía a Kestrel después de un control preliminar.

Cualquiera de las configuraciones—con o sin un servidor proxy inverso—es una configuración de hospedaje
válida y admitida para ASP.NET 2.0 o aplicaciones posteriores. Para más información, vea When to use Kestrel
with a reverse proxy (Cuándo se debe usar Kestrel con un proxy inverso).
No se puede usar IIS, Nginx o Apache sin Kestrel ni ninguna implementación de servidor personalizado.
ASP.NET Core se ha diseñado para ejecutarse en su propio proceso de modo que pueda comportarse de forma
coherente entre varias plataformas. IIS, Nginx y Apache dictan su propio procedimiento y entorno de inicio. Para
usar estas tecnologías de servidor directamente, ASP.NET Core tendría que adaptarse a los requisitos de cada
servidor. El uso de una implementación de servidor web como Kestrel proporciona a ASP.NET Core el control
sobre el entorno y el proceso de inicio cuando se hospedan en tecnologías de servidor diferentes.
IIS con Kestrel
Al usar IIS o IIS Express como proxy inverso para ASP.NET Core, la aplicación ASP.NET Core se ejecuta en un
proceso independiente del proceso de trabajo de IIS. En el proceso IIS, el módulo de ASP.NET Core coordina la
relación de proxy inverso. Las funciones principales del módulo de ASP.NET Core consisten en iniciar la
aplicación ASP.NET Core, reiniciarla cuando se bloquea y reenviar el tráfico HTTP a la aplicación. Para más
información, vea ASP.NET Core Module (Módulo de ASP.NET Core).
Nginx con Kestrel
Para información sobre cómo usar Nginx en Linux como servidor proxy inverso para Kestrel, vea Host on Linux
with Nginx (Hospedar en Linux con Nginx).
Apache con Kestrel
Para información sobre cómo usar Apache en Linux como servidor proxy inverso para Kestrel, vea Host on
Linux with Apache (Hospedar en Linux con Apache).

HTTP.sys
ASP.NET Core 2.x
ASP.NET Core 1.x
Si las aplicaciones ASP.NET Core se ejecutan en Windows, HTTP.sys es una alternativa a Kestrel. Suele
recomendarse Kestrel para un rendimiento óptimo. HTTP.sys se puede usar en escenarios en los que la
aplicación se expone a Internet y las características necesarias son compatibles con HTTP.sys pero no Kestrel.
Para información sobre las características de HTTP.sys, vea HTTP.sys.

HTTP.sys también se puede usar para las aplicaciones que solo se exponen a una red interna.

Infraestructura de servidores de ASP.NET Core


La interfaz IApplicationBuilder disponible en el método Startup.Configure expone la propiedad ServerFeatures
de tipo IFeatureCollection. Kestrel y HTTP.sys (WebListener en ASP.NET Core 1.x) solo exponen una
característica cada uno, IServerAddressesFeature, pero otras implementaciones de servidor pueden exponer
funcionalidades adicionales.
Se puede usar IServerAddressesFeature para averiguar a qué puerto se ha enlazado la implementación del
servidor en tiempo de ejecución.

Servidores personalizados
Si los servidores integrados no cumplen los requisitos de la aplicación, se puede crear una implementación de
servidor personalizado. En la guía de Open Web Interface for .NET (OWIN ) se muestra cómo escribir una
implementación de IServer basada en Nowin. Solo requieren la implementación las interfaces de características
que usa la aplicación, aunque como mínimo se debe admitir IHttpRequestFeature e IHttpResponseFeature.

Inicio del servidor


Cuando se usa Visual Studio, Visual Studio para Mac o Visual Studio Code, el servidor se inicia cuando el
entorno de desarrollo integrado (IDE ) inicia la aplicación. En Visual Studio en Windows, se pueden usar perfiles
de inicio para iniciar la aplicación y el servidor con IIS Express/módulo de ASP.NET Core o la consola. En Visual
Studio Code, Omnisharp inicia la aplicación y el servidor y activa el depurador CoreCLR. En Visual Studio para
Mac, Mono Soft-Mode Debugger inicia la aplicación y el servidor.
Al iniciar una aplicación desde un símbolo del sistema en la carpeta del proyecto, dotnet run inicia la aplicación y
el servidor (solo Kestrel y HTTP.sys). La configuración se especifica mediante la opción -c|--configuration , que
está establecida en Debug (valor predeterminado) o Release . Si no hay perfiles de inicio en un archivo
launchSettings.json, use la opción --launch-profile <NAME> para establecer el perfil de inicio (por ejemplo,
Development o Production ). Para más información, vea los temas dotnet run y Empaquetado de distribución de
.NET Core.

Recursos adicionales
Kestrel
Kestrel con IIS
Hospedaje en Linux con Nginx
Hospedaje en Linux con Apache
HTTP.sys (para ASP.NET Core 1.x, vea WebListener)
Implementación del servidor web Kestrel en
ASP.NET Core
31/05/2018 • 39 minutes to read • Edit Online

Por Tom Dykstra, Chris Ross y Stephen Halter


Kestrel es un servidor web multiplataforma de ASP.NET Core. Kestrel es el servidor web que se incluye de
forma predeterminada en las plantillas de proyecto de ASP.NET Core.
Kestrel admite las siguientes características:
HTTPS
Actualización opaca para habilitar WebSockets
Sockets de Unix para alto rendimiento detrás de Nginx
Kestrel admite todas las plataformas y versiones que sean compatibles con .NET Core.
Vea o descargue el código de ejemplo (cómo descargarlo)

Cuándo usar Kestrel con un proxy inverso


ASP.NET Core 2.x
ASP.NET Core 1.x
Puede usar Kestrel por sí solo o con un servidor proxy inverso, como IIS, Nginx o Apache. Un servidor
proxy inverso recibe las solicitudes HTTP de Internet y las reenvía a Kestrel después de un control
preliminar.

Cualquiera de las configuraciones—con o sin un servidor proxy inverso—es una configuración de


hospedaje válida y admitida para ASP.NET 2.0 o aplicaciones posteriores.
Un escenario de proxy inverso tiene lugar cuando varias aplicaciones comparten el mismo puerto y
dirección IP, y se ejecutan en un solo servidor. Este escenario no es viable con Kestrel, ya que Kestrel no
permite compartir la misma dirección IP y el mismo puerto entre varios procesos. Si Kestrel se configura
para escuchar en un puerto, controla todo el tráfico de ese puerto, independientemente del encabezado de
host de la solicitud. Un proxy inverso que puede compartir puertos es capaz de reenviar solicitudes a
Kestrel en una única dirección IP y puerto.
Aunque no sea necesario un servidor proxy inverso, su uso puede ser útil:
Puede limitar el área expuesta públicamente de las aplicaciones que hospeda.
Proporciona una capa extra de configuración y defensa.
Es posible que se integre mejor con la infraestructura existente.
Simplifica el equilibrio de carga y la configuración SSL. Solo el servidor proxy inverso requiere un
certificado SSL, y dicho servidor se puede comunicar con los servidores de aplicaciones en la red
interna por medio de HTTP sin formato.

WARNING
Si no usa un proxy inverso con el filtrado de hosts habilitado, deberá habilitarlo.

Cómo usar Kestrel en aplicaciones ASP.NET Core


ASP.NET Core 2.x
ASP.NET Core 1.x
El paquete Microsoft.AspNetCore.Server.Kestrel se incluye en el metapaquete Microsoft.AspNetCore.All.
Las plantillas de proyecto de ASP.NET Core usan Kestrel de forma predeterminada. En Program.cs, el
código de plantilla llama a CreateDefaultBuilder, que a su vez llama a UseKestrel en segundo plano.

public static void Main(string[] args)


{
BuildWebHost(args).Run();
}

public static IWebHost BuildWebHost(string[] args) =>


WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.Build();

Opciones de Kestrel
ASP.NET Core 2.x
ASP.NET Core 1.x
El servidor web Kestrel tiene opciones de configuración de restricción que son especialmente útiles en las
implementaciones con conexión a Internet. Estos son algunos de los límites importantes que se pueden
personalizar:
Las conexiones máximas de cliente
El tamaño máximo del cuerpo de solicitud
La velocidad mínima de los datos del cuerpo de solicitud.
Establezca estas y otras restricciones en la propiedad Limits de la clase KestrelServerOptions. La propiedad
Limits contiene una instancia de la clase KestrelServerLimits.

Conexiones de cliente máximas


MaxConcurrentConnections
MaxConcurrentUpgradedConnections
El número máximo de conexiones de TCP abiertas simultáneas que se pueden establecer para toda la
aplicación con este código:
.UseKestrel(options =>
{
options.Limits.MaxConcurrentConnections = 100;
options.Limits.MaxConcurrentUpgradedConnections = 100;
options.Limits.MaxRequestBodySize = 10 * 1024;
options.Limits.MinRequestBodyDataRate =
new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(10));
options.Limits.MinResponseDataRate =
new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(10));
options.Listen(IPAddress.Loopback, 5000);
options.Listen(IPAddress.Loopback, 5001, listenOptions =>
{
listenOptions.UseHttps("testCert.pfx", "testPassword");
});
})

Hay un límite independiente para las conexiones que se han actualizado desde HTTP o HTTPS a otro
protocolo (por ejemplo, en una solicitud de WebSockets). Cuando se actualiza una conexión, no se cuenta
con respecto al límite de MaxConcurrentConnections .

.UseKestrel(options =>
{
options.Limits.MaxConcurrentConnections = 100;
options.Limits.MaxConcurrentUpgradedConnections = 100;
options.Limits.MaxRequestBodySize = 10 * 1024;
options.Limits.MinRequestBodyDataRate =
new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(10));
options.Limits.MinResponseDataRate =
new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(10));
options.Listen(IPAddress.Loopback, 5000);
options.Listen(IPAddress.Loopback, 5001, listenOptions =>
{
listenOptions.UseHttps("testCert.pfx", "testPassword");
});
})

El número máximo de conexiones es ilimitado de forma predeterminada (null).


Tamaño máximo del cuerpo de la solicitud
MaxRequestBodySize
El tamaño máximo predeterminado del cuerpo de solicitud es 30 000 000 bytes, que son aproximadamente
28,6 MB.
El método recomendado para invalidar el límite de una aplicación ASP.NET Core MVC es usar el atributo
RequestSizeLimit en un método de acción:

[RequestSizeLimit(100000000)]
public IActionResult MyActionMethod()

Este es un ejemplo que muestra cómo configurar la restricción en la aplicación y todas las solicitudes:
.UseKestrel(options =>
{
options.Limits.MaxConcurrentConnections = 100;
options.Limits.MaxConcurrentUpgradedConnections = 100;
options.Limits.MaxRequestBodySize = 10 * 1024;
options.Limits.MinRequestBodyDataRate =
new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(10));
options.Limits.MinResponseDataRate =
new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(10));
options.Listen(IPAddress.Loopback, 5000);
options.Listen(IPAddress.Loopback, 5001, listenOptions =>
{
listenOptions.UseHttps("testCert.pfx", "testPassword");
});
})

Puede invalidar la configuración en una solicitud específica de middleware:

app.Run(async (context) =>


{
context.Features.Get<IHttpMaxRequestBodySizeFeature>()
.MaxRequestBodySize = 10 * 1024;
context.Features.Get<IHttpMinRequestBodyDataRateFeature>()
.MinDataRate = new MinDataRate(bytesPerSecond: 100,
gracePeriod: TimeSpan.FromSeconds(10));
context.Features.Get<IHttpMinResponseDataRateFeature>()
.MinDataRate = new MinDataRate(bytesPerSecond: 100,
gracePeriod: TimeSpan.FromSeconds(10));

Se inicia una excepción si la aplicación intenta configurar el límite de una solicitud después de que la
aplicación haya empezado a leer la solicitud. Hay una propiedad IsReadOnly que señala si la propiedad
MaxRequestBodySize tiene el estado de solo lectura, lo que significa que es demasiado tarde para configurar
el límite.
Velocidad mínima de los datos del cuerpo de la solicitud
MinRequestBodyDataRate
MinResponseDataRate
Kestrel comprueba cada segundo si los datos entran a la velocidad especificada en bytes por segundo. Si la
velocidad está por debajo del mínimo, se agota el tiempo de espera de la conexión. El período de gracia es
la cantidad de tiempo que Kestrel da al cliente para aumentar su velocidad de envío hasta el mínimo; no se
comprueba la velocidad durante ese tiempo. Este período de gracia permite evitar que se interrumpan las
conexiones que inicialmente están enviando datos a una velocidad lenta debido a un inicio lento de TCP.
La velocidad mínima predeterminada es 240 bytes por segundo, con un período de gracia de cinco
segundos.
También se aplica una velocidad mínima a la respuesta. El código para establecer el límite de solicitudes y el
límite de respuestas es el mismo, salvo que tienen RequestBody o Response en los nombres de propiedad y
de interfaz.
Este es un ejemplo que muestra cómo configurar las velocidades de datos mínimas en Program.cs:
.UseKestrel(options =>
{
options.Limits.MaxConcurrentConnections = 100;
options.Limits.MaxConcurrentUpgradedConnections = 100;
options.Limits.MaxRequestBodySize = 10 * 1024;
options.Limits.MinRequestBodyDataRate =
new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(10));
options.Limits.MinResponseDataRate =
new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(10));
options.Listen(IPAddress.Loopback, 5000);
options.Listen(IPAddress.Loopback, 5001, listenOptions =>
{
listenOptions.UseHttps("testCert.pfx", "testPassword");
});
})

Puede configurar las velocidades por solicitud de middleware:

app.Run(async (context) =>


{
context.Features.Get<IHttpMaxRequestBodySizeFeature>()
.MaxRequestBodySize = 10 * 1024;
context.Features.Get<IHttpMinRequestBodyDataRateFeature>()
.MinDataRate = new MinDataRate(bytesPerSecond: 100,
gracePeriod: TimeSpan.FromSeconds(10));
context.Features.Get<IHttpMinResponseDataRateFeature>()
.MinDataRate = new MinDataRate(bytesPerSecond: 100,
gracePeriod: TimeSpan.FromSeconds(10));

Para más información sobre otras opciones y límites de Kestrel, vea:


KestrelServerOptions
KestrelServerLimits
ListenOptions
Configuración de punto de conexión
ASP.NET Core 2.x
ASP.NET Core 1.x
ASP.NET Core se enlaza a http://localhost:5000 de forma predeterminada. Llame a los métodos Listen o
ListenUnixSocket en KestrelServerOptions para configurar los puertos y los prefijos de dirección URL de
Kestrel. UseUrls , el argumento de línea de comandos --urls , la clave de configuración de host urls y la
variable de entorno ASPNETCORE_URLS también funcionan, pero tienen las limitaciones que se indican más
adelante en esta sección.
La clave de configuración de host urls debe proceder de la configuración del host, no de la configuración
de la aplicación. Si se agrega una clave y un valor urls a appsettings.json, ello no repercute en la
configuración de host, porque el host se inicializa completamente cuando se lee la configuración del
archivo de configuración. Con todo, para configurar el host se puede usar una clave urls de
appsettings.json con UseConfiguration en el generador de hosts:
var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appSettings.json", optional: true, reloadOnChange: true)
.Build();

var host = new WebHostBuilder()


.UseKestrel()
.UseConfiguration(config)
.UseContentRoot(Directory.GetCurrentDirectory())
.UseStartup<Startup>()
.Build();

ASP.NET Core enlaza de forma predeterminada a:


http://localhost:5000
https://localhost:5001 (cuando hay presente un certificado de desarrollo local)

Un certificado de desarrollo se crea:


Cuando el SDK de .NET Core está instalado.
Para crear un certificado, se usa la herramienta dev-certs.
Algunos exploradores requieren que se conceda permiso explícito en el explorador para poder confiar en el
certificado de desarrollo local.
ASP.NET Core 2.1 y las plantillas de proyecto posteriores configuran aplicaciones para que se ejecuten en
HTTPS de forma predeterminada e incluyen redirección de HTTPS y compatibilidad con HSTS.
Llame a los métodos Listen o ListenUnixSocket en KestrelServerOptions para configurar los puertos y los
prefijos de dirección URL de Kestrel.
UseUrls , el argumento de línea de comandos --urls , la clave de configuración de host urls y la variable
de entorno ASPNETCORE_URLS también funcionan, pero tienen las limitaciones que se indican más adelante
en esta sección (debe haber disponible un certificado predeterminado para la configuración de puntos de
conexión HTTPS ).
La configuración KestrelServerOptions de ASP.NET Core 2.1:
ConfigureEndpointDefaults(Action<ListenOptions>)
Especifica una Action de configuración para que se ejecute con cada punto de conexión especificado. Al
llamar a ConfigureEndpointDefaults varias veces, se reemplazan las Action anteriores por la última
Action especificada.

ConfigureHttpsDefaults(Action<HttpsConnectionAdapterOptions>)
Especifica una Action de configuración para que se ejecute con cada punto de conexión HTTPS. Al llamar
a ConfigureHttpsDefaults varias veces, se reemplazan las Action anteriores por la última Action
especificada.
Configure(IConfiguration)
Crea un cargador de configuración para configurar Kestrel que toma una IConfiguration como entrada. El
ámbito de la configuración debe corresponderse con la sección de configuración de Kestrel.
ListenOptions.UseHttps
Configure Kestrel para que use HTTPS.
Extensiones de ListenOptions.UseHttps :
UseHttps: configure Kestrel para que use HTTPS con el certificado predeterminado. Produce una
excepción si no hay ningún certificado predeterminado configurado.
UseHttps(string fileName)
UseHttps(string fileName, string password)
UseHttps(string fileName, string password, Action<HttpsConnectionAdapterOptions> configureOptions)
UseHttps(StoreName storeName, string subject)
UseHttps(StoreName storeName, string subject, bool allowInvalid)
UseHttps(StoreName storeName, string subject, bool allowInvalid, StoreLocation location)
UseHttps(StoreName storeName, string subject, bool allowInvalid, StoreLocation location,
Action<HttpsConnectionAdapterOptions> configureOptions)
UseHttps(X509Certificate2 serverCertificate)
UseHttps(X509Certificate2 serverCertificate, Action<HttpsConnectionAdapterOptions> configureOptions)
UseHttps(Action<HttpsConnectionAdapterOptions> configureOptions)

Parámetros de ListenOptions.UseHttps :
filename es la ruta de acceso y el nombre de archivo de un archivo de certificado correspondiente al
directorio donde están los archivos de contenido de la aplicación.
password es la contraseña necesaria para obtener acceso a los datos del certificado X.509.
configureOptions es una Action para configurar HttpsConnectionAdapterOptions . Devuelve
ListenOptions .
storeName es el almacén de certificados desde el que se carga el certificado.
subject es el nombre del sujeto del certificado.
allowInvalid indica si se deben tener en cuenta los certificados no válidos, como los certificados
autofirmados.
location es la ubicación del almacén desde el que se carga el certificado.
serverCertificate es el certificado X.509.

En un entorno de producción, HTTPS se debe configurar explícitamente. Como mínimo, debe existir un
certificado predeterminado.
Estas son las configuraciones compatibles:
Sin configuración
Reemplazar el certificado predeterminado de configuración
Cambiar los valores predeterminados en el código
Sin configuración
Kestrel escucha en http://localhost:5000 y en https://localhost:5001 (si hay disponible un certificado
predeterminado).
Especifique direcciones URL mediante los siguientes elementos:
La variable de entorno ASPNETCORE_URLS .
El argumento de la línea de comandos --urls .
La clave de configuración de host urls .
El método de extensión UseUrls .

Para obtener más información, vea Direcciones URL del servidor e Invalidar la configuración.
El valor que estos métodos suministran puede ser uno o más puntos de conexión HTTP y HTTPS (este
último, si hay disponible un certificado predeterminado). Configure el valor como una lista separada por
punto y coma (por ejemplo, "Urls": "http://localhost:8000;http://localhost:8001" ).
Reemplazar el certificado predeterminado de configuración
WebHost.CreateDefaultBuilder llama a
serverOptions.Configure(context.Configuration.GetSection("Kestrel")) de forma predeterminada para
cargar la configuración de Kestrel. Hay disponible un esquema de configuración de aplicación HTTPS
predeterminado para Kestrel. Configure varios puntos de conexión (incluidas las direcciones URL y los
certificados que va a usar) desde un archivo en disco o desde un almacén de certificados.
En el siguiente ejemplo de appsettings.json:
Establezca AllowInvalid en true para permitir el uso de certificados no válidos (por ejemplo,
certificados autofirmados).
Cualquier punto de conexión HTTPS que no especifique un certificado (HttpsDefaultCert en el
siguiente ejemplo) revierte al certificado definido en Certificados > Predeterminado o al certificado
de desarrollo.

{
"Kestrel": {
"EndPoints": {
"Http": {
"Url": "http://localhost:5000"
},

"HttpsInlineCertFile": {
"Url": "https://localhost:5001",
"Certificate": {
"Path": "<path to .pfx file>",
"Password": "<certificate password>"
}
},

"HttpsInlineCertStore": {
"Url": "https://localhost:5002",
"Certificate": {
"Subject": "<subject; required>",
"Store": "<certificate store; defaults to My>",
"Location": "<location; defaults to CurrentUser>",
"AllowInvalid": "<true or false; defaults to false>"
}
},

"HttpsDefaultCert": {
"Url": "https://localhost:5003"
},

"Https": {
"Url": "https://*:5004",
"Certificate": {
"Path": "<path to .pfx file>",
"Password": "<certificate password>"
}
}
},
"Certificates": {
"Default": {
"Path": "<path to .pfx file>",
"Password": "<certificate password>"
}
}
}
}

Una alternativa al uso de Path y Password en cualquier nodo de certificado consiste en especificar el
certificado por medio de campos del almacén de certificados. Por ejemplo, el certificado en Certificados >
Predeterminado se puede especificar así:
"Default": {
"Subject": "<subject; required>",
"Store": "<cert store; defaults to My>",
"Location": "<location; defaults to CurrentUser>",
"AllowInvalid": "<true or false; defaults to false>"
}

Notas sobre el esquema:


En los nombres de los puntos de conexión se distingue entre mayúsculas y minúsculas. Por ejemplo,
HTTPS y Https son válidos.
El parámetro Url es necesario en cada punto de conexión. El formato de este parámetro es el mismo
que el del parámetro de configuración Urls de nivel superior, excepto por el hecho de que está limitado
a un único valor.
En vez de agregarse, estos puntos de conexión reemplazan a los que están definidos en la configuración
Urls de nivel superior. Los puntos de conexión definidos en el código a través de Listen son
acumulativos con respecto a los puntos de conexión definidos en la sección de configuración.
La sección Certificate es opcional. Si la sección Certificate no se especifica, se usan los valores
predeterminados definidos en escenarios anteriores. Si no hay valores predeterminados disponibles, el
servidor produce una excepción y no se inicia.
La sección Certificate admite certificados tanto Path–Password como Subject–Store.
Se puede definir el número de puntos de conexión que se quiera de esta manera, siempre y cuando no
produzcan conflictos de puerto.
serverOptions.Configure(context.Configuration.GetSection("Kestrel")) devuelve un
KestrelConfigurationLoader con un método .Endpoint(string name, options => { }) que se puede usar
para complementar la configuración de un punto de conexión configurado:

serverOptions.Configure(context.Configuration.GetSection("Kestrel"))
.Endpoint("HTTPS", opt =>
{
opt.HttpsOptions.SslProtocols = SslProtocols.Tls12;
});

También puede tener acceso directamente a KestrelServerOptions.ConfigurationLoader para proseguir con


la iteración en el cargador existente, como la proporcionada por WebHost.CreatedDeafaultBuilder .
La sección de configuración de cada punto de conexión está disponible en las opciones del método
Endpoint para que se pueda leer la configuración personalizada.
Se pueden cargar varias configuraciones volviendo a llamar a
serverOptions.Configure(context.Configuration.GetSection("Kestrel")) con otra sección. Se usa la última
configuración, a menos que se llame explícitamente a Load en instancias anteriores. El metapaquete no
llama a Load , con lo cual su sección de configuración predeterminada se puede reemplazar.
KestrelConfigurationLoader refleja la familia Listen de API de KestrelServerOptions como
sobrecargas de Endpoint , por lo que los puntos de conexión de configuración y código se pueden
configurar en el mismo lugar. En estas sobrecargas no se usan nombres y solo consumen valores
predeterminados de la configuración.
Cambiar los valores predeterminados en el código
ConfigureEndpointDefaults y ConfigureHttpsDefaultsse pueden usar para cambiar la configuración
predeterminada de ListenOptions y HttpsConnectionAdapterOptions , incluido sustituir el certificado
predeterminado especificado en el escenario anterior. Se debe llamar a ConfigureEndpointDefaults y a
ConfigureHttpsDefaults antes de que se configure algún punto de conexión.
options.ConfigureEndpointDefaults(opt =>
{
opt.NoDelay = true;
});

options.ConfigureHttpsDefaults(httpsOptions =>
{
httpsOptions.SslProtocols = SslProtocols.Tls12;
});

Compatibilidad de Kestrel con SNI


Indicación de nombre de servidor (SNI) se puede usar para hospedar varios dominios en la misma
dirección IP y puerto. Para que SNI funcione, el cliente envía el nombre de host de la sesión segura al
servidor durante el protocolo de enlace TLS para que, de este modo, el servidor pueda proporcionar el
certificado correcto. El cliente usa el certificado proporcionado para la comunicación cifrada con el servidor
durante la sesión segura que sigue al protocolo de enlace TLS.
Kestrel admite SNI a través de la devolución de llamada ServerCertificateSelector . La devolución de
llamada se invoca una vez por conexión para permitir que la aplicación inspeccione el nombre de host y
seleccione el certificado adecuado.
La compatibilidad con SNI requiere la ejecución en la plataforma de destino netcoreapp2.1 . En
netcoreapp2.0 y net461 , se invoca la devolución de llamada, pero name siempre es null . name también
será null si el cliente no proporciona el parámetro de nombre de host en el protocolo de enlace TLS.

WebHost.CreateDefaultBuilder()
.UseKestrel((context, options) =>
{
options.ListenAnyIP(5005, listenOptions =>
{
listenOptions.UseHttps(httpsOptions =>
{
var localhostCert = CertificateLoader.LoadFromStoreCert(
"localhost", "My", StoreLocation.CurrentUser,
allowInvalid: true);
var exampleCert = CertificateLoader.LoadFromStoreCert(
"example.com", "My", StoreLocation.CurrentUser,
allowInvalid: true);
var subExampleCert = CertificateLoader.LoadFromStoreCert(
"sub.example.com", "My", StoreLocation.CurrentUser,
allowInvalid: true);
var certs = new Dictionary(StringComparer.OrdinalIgnoreCase);
certs["localhost"] = localhostCert;
certs["example.com"] = exampleCert;
certs["sub.example.com"] = subExampleCert;

httpsOptions.ServerCertificateSelector = (connectionContext, name) =>


{
if (name != null && certs.TryGetValue(name, out var cert))
{
return cert;
}

return exampleCert;
};
});
});
});

Enlazar a un socket TCP


El método Listen se enlaza a un socket TCP y una lambda de opciones hace posible la configuración de un
certificado SSL:

public static void Main(string[] args)


{
BuildWebHost(args).Run();
}

public static IWebHost BuildWebHost(string[] args) =>


WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.UseKestrel(options =>
{
options.Listen(IPAddress.Loopback, 8000);
options.Listen(IPAddress.Loopback, 8001, listenOptions =>
{
listenOptions.UseHttps("testCert.pfx", "testPassword");
});
})
.Build();

En el ejemplo se configura SSL para un punto de conexión con ListenOptions. Use la misma API para
configurar otras opciones de Kestrel para puntos de conexión específicos.
En Windows, pueden crearse certificados autofirmados con el cmdlet New -SelfSignedCertificate de
PowerShell. Para obtener un ejemplo no compatible, vea UpdateIISExpressSSLForChrome.ps1.
En macOS, Linux y Windows, pueden crearse certificados con OpenSSL.
Enlazar a un socket de Unix
Escuche en un socket de Unix con ListenUnixSocket para mejorar el rendimiento con Nginx, tal y como se
muestra en este ejemplo:

.UseKestrel(options =>
{
options.ListenUnixSocket("/tmp/kestrel-test.sock");
options.ListenUnixSocket("/tmp/kestrel-test.sock", listenOptions =>
{
listenOptions.UseHttps("testCert.pfx", "testpassword");
});
})

Puerto 0
Cuando se especifica el número de puerto 0 , Kestrel se enlaza de forma dinámica a un puerto disponible.
En el siguiente ejemplo se muestra cómo averiguar qué puerto Kestrel está realmente enlazado a un
runtime:

Cuando la aplicación se ejecuta, la salida de la ventana de consola indica el puerto dinámico en el que se
puede tener acceso a la aplicación:

Now listening on: http://127.0.0.1:48508

UseUrls, argumento de línea de comandos --urls, clave de configuración de host urls y


limitaciones de la variable de entorno ASPNETCORE_URLS
Configure puntos de conexión con los siguientes métodos:
UseUrls
El argumento de la línea de comandos --urls
La clave de configuración de host urls
La variable de entorno ASPNETCORE_URLS

Estos métodos son útiles para que el código funcione con servidores que no sean de Kestrel. Pero tenga en
cuenta estas limitaciones:
SSL no se puede usar con estos métodos, a menos que se proporcione un certificado predeterminado
en la configuración del punto de conexión HTTPS (por ejemplo, por medio de la configuración
KestrelServerOptions o de un archivo de configuración, tal y como se explicó anteriormente en este
tema).
Cuando los métodos Listen y UseUrls se usan al mismo tiempo, los puntos de conexión de Listen
sustituyen a los de UseUrls .
Configuración de puntos de conexión IIS
Cuando se usa IIS, los enlaces de direcciones URL de IIS reemplazan a los enlaces que se hayan
establecido por medio de Listen o de UseUrls . Para más información, vea el tema Módulo ASP.NET Core.

Configuración de transporte
Desde el lanzamiento de ASP.NET Core 2.1, el transporte predeterminado de Kestrel deja de basarse en
Libuv y pasa a basarse en sockets administrados. Se trata de un cambio muy importante para las
aplicaciones ASP.NET Core 2.0 que actualizan a 2.1 y llaman a WebHostBuilderLibuvExtensions.UseLibuv,
y que dependen de cualquiera de los siguientes paquetes:
Microsoft.AspNetCore.Server.Kestrel (referencia de paquete directa)
Microsoft.AspNetCore.App
En el caso de los proyectos de ASP.NET Core 2.1 o posterior que usan el metapaquete
Microsoft.AspNetCore.App y requieren el uso de Libuv:

Agregar una dependencia del paquete Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv al archivo


de proyecto de la aplicación:

<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv"
Version="2.1.0" />

Llame a WebHostBuilderLibuvExtensions.UseLibuv:

public class Program


{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>


WebHost.CreateDefaultBuilder(args)
.UseLibuv()
.UseStartup<Startup>();
}
Prefijos de URL
Al usar UseUrls , el argumento de línea de comandos --urls , la clave de configuración de host urls o
una variable de entorno ASPNETCORE_URLS , los prefijos de dirección URL pueden tener cualquiera de estos
formatos.
ASP.NET Core 2.x
ASP.NET Core 1.x
Solo son válidos los prefijos de dirección URL HTTP. Kestrel no admite SSL cuando se configuran enlaces
de dirección URL con UseUrls .
Dirección IPv4 con número de puerto

http://65.55.39.10:80/

0.0.0.0 es un caso especial que enlaza a todas las direcciones IPv4.


Dirección IPv6 con número de puerto

http://[0:0:0:0:0:ffff:4137:270a]:80/

[::] es el equivalente en IPv6 de 0.0.0.0 en IPv4.


Nombre de host con número de puerto

http://contoso.com:80/
http://*:80/

Los nombres de host, * y + no son especiales. Todo lo que no se identifique como una dirección
IP o un localhost válido se enlaza a todas las direcciones IP de IPv6 e IPv4. Para enlazar distintos
nombres de host a distintas aplicaciones ASP.NET Core en el mismo puerto, use HTTP.sys o un
servidor proxy inverso, como IIS, Nginx o Apache.

WARNING
Si no usa un proxy inverso con el filtrado de hosts habilitado, habilítelo.

Nombre localhost del host con el número de puerto o la IP de bucle invertido con el número de
puerto

http://localhost:5000/
http://127.0.0.1:5000/
http://[::1]:5000/

Cuando se especifica localhost , Kestrel intenta enlazar a las interfaces de bucle invertido de IPv4 e
IPv6. Si el puerto solicitado lo está usando otro servicio en cualquier interfaz de bucle invertido,
Kestrel no se puede iniciar. Si ninguna de estas interfaces de bucle invertido está disponible por
cualquier otra razón (normalmente porque no se admite IPv6), Kestrel registra una advertencia.

Filtrado de hosts
Si bien Kestrel admite una configuración basada en prefijos como http://example.com:5000 , pasa por alto
completamente el nombre de host. El host localhost es un caso especial que se usa para enlazar a
direcciones de bucle invertido. Cualquier otro host que no sea una dirección IP explícita se enlaza a todas
las direcciones IP públicas. Ninguno de estos datos se usa para validar encabezados Host de solicitudes.
Hay dos soluciones alternativas:
Host detrás de un proxy inverso con filtrado de encabezados de host. Este ha sido el único escenario
admitido para Kestrel en ASP.NET Core 1.x.
Usar un middleware para filtrar las solicitudes por el encabezado Host . Este es un middleware de
ejemplo:

using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

// A normal middleware would provide an options type, config binding, extension methods, etc..
// This intentionally does all of the work inside of the middleware so it can be
// easily copy-pasted into docs and other projects.
public class HostFilteringMiddleware
{
private readonly RequestDelegate _next;
private readonly IList<string> _hosts;
private readonly ILogger<HostFilteringMiddleware> _logger;

public HostFilteringMiddleware(RequestDelegate next, IConfiguration config,


ILogger<HostFilteringMiddleware> logger)
{
if (config == null)
{
throw new ArgumentNullException(nameof(config));
}

_next = next ?? throw new ArgumentNullException(nameof(next));


_logger = logger ?? throw new ArgumentNullException(nameof(logger));

// A semicolon separated list of host names without the port numbers.


// IPv6 addresses must use the bounding brackets and be in their normalized form.
_hosts = config["AllowedHosts"]?.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
if (_hosts == null || _hosts.Count == 0)
{
throw new InvalidOperationException("No configuration entry found for AllowedHosts.");
}
}

public Task Invoke(HttpContext context)


{
if (!ValidateHost(context))
{
context.Response.StatusCode = 400;
_logger.LogDebug("Request rejected due to incorrect Host header.");
return Task.CompletedTask;
}

return _next(context);
}

// This does not duplicate format validations that are expected to be performed by the host.
private bool ValidateHost(HttpContext context)
{
StringSegment host = context.Request.Headers[HeaderNames.Host].ToString().Trim();
if (StringSegment.IsNullOrEmpty(host))
{
// Http/1.0 does not require the Host header.
// Http/1.1 requires the header but the value may be empty.
return true;
}

// Drop the port

var colonIndex = host.LastIndexOf(':');

// IPv6 special case


if (host.StartsWith("[", StringComparison.Ordinal))
{
var endBracketIndex = host.IndexOf(']');
if (endBracketIndex < 0)
{
// Invalid format
return false;
}
if (colonIndex < endBracketIndex)
{
// No port, just the IPv6 Host
colonIndex = -1;
}
}

if (colonIndex > 0)
{
host = host.Subsegment(0, colonIndex);
}

foreach (var allowedHost in _hosts)


{
if (StringSegment.Equals(allowedHost, host, StringComparison.OrdinalIgnoreCase))
{
return true;
}

// Sub-domain wildcards: *.example.com


if (allowedHost.StartsWith("*.", StringComparison.Ordinal) && host.Length >=
allowedHost.Length)
{
// .example.com
var allowedRoot = new StringSegment(allowedHost, 1, allowedHost.Length - 1);

var hostRoot = host.Subsegment(host.Length - allowedRoot.Length, allowedRoot.Length);


if (hostRoot.Equals(allowedRoot, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
}

return false;
}
}

Registre el HostFilteringMiddleware anterior en Startup.Configure . Cabe mencionar que el orden de


registro del middleware es importante. debe tener lugar inmediatamente después del registro del
middleware de diagnóstico (por ejemplo, app.UseExceptionHandler ).
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}

app.UseMiddleware<HostFilteringMiddleware>();

app.UseMvcWithDefaultRoute();
}

El middleware anterior espera una clave AllowedHosts en appsettings.<NombreEntorno>.json. El valor de


esta clave es una lista delimitada por punto y coma de nombres de host sin los números de puerto. Incluya
el par clave-valor AllowedHosts en appsettings.Production.json:

{
"AllowedHosts": "example.com"
}

appsettings.Development.json (archivo de configuración de localhost):

{
"AllowedHosts": "localhost"
}

Recursos adicionales
Aplicación de HTTPS
Código fuente de Kestrel
Módulo ASP.NET Core
25/06/2018 • 4 minutes to read • Edit Online

Por Tom Dykstra, Rick Strahl y Chris Ross


El módulo ASP.NET Core permite a las aplicaciones ASP.NET Core ejecutarse tras IIS en una
configuración de proxy inverso. IIS proporciona características de administración y seguridad de
aplicaciones web avanzadas.
Versiones de Windows compatibles:
Windows 7 o posterior
Windows Server 2008 R2 o posterior
El módulo ASP.NET Core funciona únicamente con Kestrel. El módulo no es compatible con HTTP.sys
(anteriormente denominado WebListener).

Descripción del módulo ASP.NET Core


El módulo ASP.NET Core es un módulo de IIS nativo que se conecta a la canalización IIS para redirigir
solicitudes web a aplicaciones ASP.NET Core de back-end. Muchos de los módulos nativos, como la
autenticación de Windows, permanecen activos. Para más información sobre los módulos de IIS activos
con el módulo, vea IIS modules (Módulos de IIS ).
Dado que las aplicaciones ASP.NET Core se ejecutan en un proceso independiente del proceso de
trabajo de IIS, el módulo también se encarga de la administración de procesos. El módulo inicia el
proceso de la aplicación ASP.NET Core cuando entra la primera solicitud, y reinicia la aplicación si esta se
bloquea. Este comportamiento es básicamente el mismo que el de las aplicaciones ASP.NET 4.x que se
ejecutan en el proceso en IIS y se administran a través del Servicio de activación de procesos de
Windows (WAS ).
En el siguiente diagrama se muestra la relación entre las aplicaciones IIS, el módulo ASP.NET Core y las
aplicaciones ASP.NET Core:

Las solicitudes llegan procedentes de Internet al controlador HTTP.sys en modo kernel. El controlador
enruta las solicitudes a IIS en el puerto configurado del sitio web, que suele ser el puerto 80 (HTTP ) o
443 (HTTPS ). El módulo reenvía las solicitudes a Kestrel en un puerto aleatorio de la aplicación, que no
es ni 80 ni 443.
El módulo especifica el puerto a través de la variable de entorno en el inicio y el middleware de
integración de IIS configura el servidor para que escuche en http://localhost:{port} . Se realizan más
comprobaciones y se rechazan las solicitudes que no se hayan originado en el módulo. El módulo no
admite el reenvío de HTTPS, por lo que las solicitudes se reenvían a través de HTTP, aunque IIS las
reciba a través de HTTPS.
Una vez que Kestrel toma una solicitud del módulo, la envía a la canalización de middleware de ASP.NET
Core. La canalización de middleware controla la solicitud y la pasa como una instancia de HttpContext a
la lógica de la aplicación. La respuesta de la aplicación se vuelve a pasar a IIS, que la envía de nuevo al
cliente HTTP que inició la solicitud.
El módulo ASP.NET Core tiene otras funciones. Puede:
Establecer variables de entorno para un proceso de trabajo.
Registrar la salida en un almacenamiento de archivos para solucionar problemas de inicio.
Reenviar tokens de autenticación de Windows.

Cómo instalar y usar el módulo ASP.NET Core


Para obtener instrucciones detalladas sobre cómo instalar y usar el módulo ASP.NET Core, vea
Hospedaje de ASP.NET Core en Windows con IIS. Para más información sobre cómo configurar el
módulo, vea ASP.NET Core Module configuration reference (Referencia de configuración del módulo de
ASP.NET Core).

Recursos adicionales
Hospedaje en Windows con IIS
Referencia de configuración del módulo ASP.NET Core
Repositorio GitHub del módulo ASP.NET Core (código fuente)
Implementación del servidor web HTTP.sys en
ASP.NET Core
25/05/2018 • 14 minutes to read • Edit Online

Por Tom Dykstra, Chris Ross y Stephen Halter

NOTE
Este tema solo se aplica a ASP.NET Core 2.0 o posterior. En versiones anteriores de ASP.NET Core, HTTP.sys se
denominaba WebListener.

HTTP.sys es un servidor web de ASP.NET Core que solo se ejecuta en Windows. HTTP.sys supone una
alternativa a Kestrel y ofrece algunas características que este no facilita.

IMPORTANT
HTTP.sys no es compatible con el módulo ASP.NET Core y no se puede usar con IIS o IIS Express.

HTTP.sys admite las siguientes características:


Autenticación de Windows
Uso compartido de puertos
HTTPS con SNI
HTTP/2 a través de TLS (Windows 10 o posterior)
Transmisión directa de archivos
Almacenamiento en caché de respuestas
WebSockets (Windows 8 o posterior)
Versiones de Windows compatibles:
Windows 7 o posterior
Windows Server 2008 R2 o posterior
Vea o descargue el código de ejemplo (cómo descargarlo)

Cuándo usar HTTP.sys


HTTP.sys resulta útil para implementaciones en las que:
Es necesario exponer el servidor directamente a Internet sin usar IIS.

Una implementación interna requiere una característica que no está disponible en Kestrel, como
Autenticación de Windows.
HTTP.sys es una tecnología consolidada que protege contra muchos tipos de ataques y que proporciona la
solidez, la seguridad y la escalabilidad de un servidor web con todas las características. El propio IIS se
ejecuta como agente de escucha de HTTP sobre HTTP.sys.

Cómo usar HTTP.sys


Configuración de la aplicación de ASP.NET Core para usar HTTP.sys
1. No se requiere una referencia de paquete en el archivo de proyecto cuando se usa el metapaquete
Microsoft.AspNetCore.All (nuget.org) (ASP.NET Core 2.0 o posterior). Si no usa el metapaquete
Microsoft.AspNetCore.All , agregue una referencia de paquete a
Microsoft.AspNetCore.Server.HttpSys.
2. Llame al método de extensión UseHttpSys extensión al compilar el host de web y especifique las
opciones de HTTP.sys necesarias:

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>


WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.UseHttpSys(options =>
{
// The following options are set to default values.
options.Authentication.Schemes = AuthenticationSchemes.None;
options.Authentication.AllowAnonymous = true;
options.MaxConnections = null;
options.MaxRequestBodySize = 30000000;
options.UrlPrefixes.Add("http://localhost:5000");
});

La configuración adicional de HTTP.sys se controla a través de Configuración del Registro.


Opciones de HTTP.sys

PROPERTY DESCRIPTION DEFAULT

AllowSynchronousIO Controlar si se permite la true


entrada/salida sincrónica de los
objetos
HttpContext.Request.Body y
HttpContext.Response.Body .

Authentication.AllowAnonymous Permitir solicitudes anónimas. true

Authentication.Schemes Especificar los esquemas de None


autenticación permitidos. Puede
modificarse en cualquier momento
antes de eliminar el agente de
escucha. Los valores se
proporcionan con la enumeración
AuthenticationSchemes: Basic ,
Kerberos , Negotiate , None y
NTLM .
PROPERTY DESCRIPTION DEFAULT

EnableResponseCaching Intentar el almacenamiento en true


memoria caché en modo kernel de
las respuestas con encabezados
elegibles. Es posible que la
respuesta no incluya encabezados
Set-Cookie , Vary o Pragma .
Debe incluir un encabezado
Cache-Control que sea public
y un valor shared-max-age o
max-age , o un encabezado
Expires .

MaxAccepts Número máximo de aceptaciones Cinco × entorno.


simultáneas. ProcessorCount

MaxConnections Establecer el número máximo de null


conexiones simultáneas que se (ilimitado)
aceptan. Use -1 para infinito. Use
null para usar la configuración
de la máquina del Registro.

MaxRequestBodySize Vea la sección 30 000 000 bytes


MaxRequestBodySize. (~28,6 MB)

RequestQueueLimit Número máximo de solicitudes que 1000


se pueden poner en cola.

ThrowWriteExceptions Indicar si las escrituras del cuerpo false


de respuesta que no se producen (finalizar con normalidad)
debido a desconexiones del cliente
deben iniciar excepciones o finalizar
con normalidad.
PROPERTY DESCRIPTION DEFAULT

Timeouts Exponer la configuración de


TimeoutManager de HTTP.sys, que
también puede configurarse en el
Registro. Siga los vínculos de API
para obtener más información
sobre cada configuración, incluidos
los valores predeterminados:
Timeouts.DrainEntityBody –
Tiempo permitido para que
la API HTTP Server purgue
el cuerpo de la entidad en
una conexión persistente.
Timeouts.EntityBody –
Tiempo permitido para que
llegue el cuerpo de la
entidad de solicitud.
Timeouts.HeaderWait –
Tiempo permitido para que
la API HTTP Server analice
el encabezado de solicitud.
Timeouts.IdleConnection –
Tiempo permitido para una
conexión inactiva.
Timeouts.MinSendBytesPer
Second – Tasa mínima de
envío de la respuesta.
Timeouts.RequestQueue –
Tiempo permitido para que
la solicitud permanezca en
la cola de solicitudes antes
de que la aplicación la
recoja.

UrlPrefixes Especifique el objeto


UrlPrefixCollection que se registra
con HTTP.sys. El más útil es
UrlPrefixCollection.Add, que se usa
para agregar un prefijo a la
colección. Pueden modificarse en
cualquier momento antes de
eliminar el agente de escucha.

MaxRequestBodySize
Tamaño máximo permitido de cualquier cuerpo de solicitud en bytes. Cuando se establece en null ,
el tamaño máximo del cuerpo de solicitud es ilimitado. Este límite no tiene ningún efecto en las
conexiones actualizadas, que siempre son ilimitadas.
El método recomendado para reemplazar el límite de una aplicación de ASP.NET Core MVC para un
solo objeto IActionResult consiste en usar el atributo RequestSizeLimitAttribute en un método de
acción:

[RequestSizeLimit(100000000)]
public IActionResult MyActionMethod()

Se inicia una excepción si la aplicación intenta configurar el límite de una solicitud después de que la
aplicación haya empezado a leer la solicitud. Puede usarse una propiedad IsReadOnly que indique si
la propiedad MaxRequestBodySize tiene el estado de solo lectura, lo que significa que es demasiado
tarde para configurar el límite.
Si la aplicación debe reemplazar MaxRequestBodySize por solicitud, use la interfaz
IHttpMaxRequestBodySizeFeature:

public void Configure(IApplicationBuilder app, IHostingEnvironment env,


ILogger<Startup> logger)
{
app.Use(async (context, next) =>
{
context.Features.Get<IHttpMaxRequestBodySizeFeature>()
.MaxRequestBodySize = 10 * 1024;

var serverAddressesFeature = app.ServerFeatures.Get<IServerAddressesFeature>();


var addresses = string.Join(", ", serverAddressesFeature?.Addresses);

logger.LogInformation($"Addresses: {addresses}");

await next.Invoke();
});

if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

// Enable HTTPS Redirection Middleware when hosting the app securely.


//app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseMvc();
}

3. If usa Visual Studio, asegúrese de que la aplicación no está configurada para ejecutar IIS o IIS
Express.
En Visual Studio, el perfil de inicio predeterminado corresponde a IIS Express. Para ejecutar el
proyecto como aplicación de consola, cambie manualmente el perfil seleccionado, tal y como se
muestra en la siguiente captura de pantalla:

Configurar Windows Server


1. Si la aplicación es una implementación dependiente del marco, instale .NET Core, .NET Framework o
ambos (si se trata de una aplicación de .NET Core que tiene como destino .NET Framework).
.NET Core – si la aplicación requiere .NET Core, obtenga y ejecute el instalador de .NET Core
desde las descargas de .NET.
.NET Framework – Si la aplicación requiere .NET Framework, vea .NET Framework: Guía de
instalación para encontrar las instrucciones de instalación. Instale la versión necesaria de .NET
Framework. El instalador de la versión más reciente de .NET Framework se puede encontrar en las
descargas de .NET.
2. Configure los puertos y las direcciones URL de la aplicación.
ASP.NET Core se enlaza a http://localhost:5000 de forma predeterminada. Para configurar los
puertos y los prefijos de dirección URL, las opciones incluyen el uso de:
UseUrls
El argumento de la línea de comandos urls
La variable de entorno ASPNETCORE_URLS
UrlPrefixes
En el ejemplo de código siguiente se muestra cómo se usa UrlPrefixes:

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>


WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.UseHttpSys(options =>
{
// The following options are set to default values.
options.Authentication.Schemes = AuthenticationSchemes.None;
options.Authentication.AllowAnonymous = true;
options.MaxConnections = null;
options.MaxRequestBodySize = 30000000;
options.UrlPrefixes.Add("http://localhost:5000");
});

Una ventaja de UrlPrefixes es que se genera inmediatamente un mensaje de error para prefijos con
formato incorrecto.
La configuración de UrlPrefixes invalida la configuración de UseUrls / urls / ASPNETCORE_URLS . Por
lo tanto, la ventaja de UseUrls , urls y la variable de entorno ASPNETCORE_URLS es que resulta más
fácil de cambiar entre Kestrel y HTTP.sys. Para obtener más información sobre UseUrls , urls y
ASPNETCORE_URLS , vea el tema Hospedaje en ASP.NET Core.

HTTP.sys usa los formatos de cadena UrlPrefix de la API HTTP Server.

WARNING
Los enlaces de carácter comodín de nivel superior ( http://*:80/ y http://+:80 ) no se deben usar. Los
enlaces de carácter comodín de nivel superior pueden exponer su aplicación a vulnerabilidades de seguridad.
Esto se aplica tanto a los caracteres comodín fuertes como a los débiles. Use nombres de host explícitos en
lugar de caracteres comodín. Los enlaces de carácter comodín de subdominio (por ejemplo, *.mysub.com ) no
suponen este riesgo de seguridad si se controla todo el dominio primario (a diferencia de *.com , que sí es
vulnerable). Vea la sección 5.4 de RFC 7230 para obtener más información.

3. Registre previamente los prefijos de dirección URL para enlazarse a HTTP.sys y configurar los
certificados x.509.
Si los prefijos de dirección URL no se han registrado previamente en Windows, ejecute la aplicación
con privilegios de administrador. La única excepción se produce al enlazarse a localhost con HTTP (no
HTTPS ) con un número de puerto superior a 1024. En ese caso, no se requieren privilegios de
administrador.
a. La herramienta integrada para configurar HTTP.sys es netsh.exe. netsh.exe se usa para reservar
prefijos de dirección URL y asignar certificados X.509. Esta herramienta requiere privilegios de
administrador.
En este ejemplo se muestran los comandos para reservar prefijos de dirección URL para los
puertos 80 y 443:

netsh http add urlacl url=http://+:80/ user=Users


netsh http add urlacl url=https://+:443/ user=Users

En este ejemplo se muestra cómo asignar un certificado X.509:

netsh http add sslcert ipport=0.0.0.0:443 certhash=MyCertHash_Here appid="{00000000-0000-


0000-0000-000000000000}"

Documentación de referencia de netsh.exe:


Comandos Netsh para protocolo de transferencia de hipertexto (HTTP )
Cadenas de UrlPrefix
b. Si es necesario, cree certificados X.509 autofirmados.
En Windows, pueden crearse certificados autofirmados con el cmdlet New -
SelfSignedCertificate de PowerShell. Para obtener un ejemplo no compatible, vea
UpdateIISExpressSSLForChrome.ps1.
En macOS, Linux y Windows, pueden crearse certificados con OpenSSL.
4. Abra puertos del firewall para permitir que el tráfico llegue a HTTP.sys. Use netsh.exe o cmdlets de
PowerShell.

Escenarios de servidor proxy y equilibrador de carga


En el caso de las aplicaciones hospedadas por HTTP.sys que interactúan con las solicitudes de Internet o de
una red corporativa, puede que sea necesario configurar más elementos si esas aplicaciones se hospedan
detrás de servidores proxy y equilibradores de carga. Para más información, vea Configurar ASP.NET Core
para trabajar con servidores proxy y equilibradores de carga.

Recursos adicionales
API HTTP Server
Repositorio aspnet/HttpSysServer de GitHub (código fuente)
Hospedaje en ASP.NET Core
Globalización y localización en ASP.NET Core
25/06/2018 • 31 minutes to read • Edit Online

By Rick Anderson, Damien Bowden, Bart Calixto, Nadeem Afana y Hisham Bin Ateya
El hecho de crear un sitio web multilingüe con ASP.NET Core permite que este llegue a un público más
amplio. ASP.NET Core proporciona servicios y software intermedio para la localización en diferentes idiomas
y referencias culturales.
La internacionalización conlleva globalización y localización. La globalización es el proceso de diseñar
aplicaciones que admiten diferentes referencias culturales. La globalización agrega compatibilidad con la
entrada, la visualización y la salida de un conjunto definido de scripts de lenguaje relacionados con áreas
geográficas específicas.
La localización es el proceso de adaptar una aplicación globalizada, que ya se ha procesado para la
localizabilidad, a una determinada referencia cultural o configuración regional. Para más información, vea
Términos relacionados con la globalización y la localización al final de este documento.
La localización de la aplicación implica lo siguiente:
1. Hacer que el contenido de la aplicación sea localizable
2. Proporcionar recursos localizados para los idiomas y las referencias culturales admitidos
3. Implementar una estrategia para seleccionar el idioma o la referencia cultural de cada solicitud

Hacer que el contenido de la aplicación sea localizable


IStringLocalizer y IStringLocalizer<T> , introducidos en ASP.NET Core, están diseñados para mejorar la
productividad al desarrollar aplicaciones localizadas. IStringLocalizer usa ResourceManager y
ResourceReader para proporcionar recursos específicos de la referencia cultural en tiempo de ejecución. La
interfaz simple tiene un indizador y un IEnumerable para devolver las cadenas localizadas. IStringLocalizer
no necesita que se almacenen las cadenas de idioma predeterminado en un archivo de recursos. Puede
desarrollar una aplicación destinada a la localización sin necesidad de crear archivos de recursos al principio
de la fase de desarrollo. En el código siguiente se muestra cómo ajustar la cadena "About Title" para la
localización.
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;

namespace Localization.StarterWeb.Controllers
{
[Route("api/[controller]")]
public class AboutController : Controller
{
private readonly IStringLocalizer<AboutController> _localizer;

public AboutController(IStringLocalizer<AboutController> localizer)


{
_localizer = localizer;
}

[HttpGet]
public string Get()
{
return _localizer["About Title"];
}
}
}

En el código anterior, la implementación de IStringLocalizer<T> procede de la inserción de dependencias. Si


no se encuentra el valor localizado de "About Title, se devuelve la clave de indizador, es decir, la cadena "About
Title". Puede dejar las cadenas literales del idioma predeterminado en la aplicación y ajustarlas en el
localizador, de modo que se pueda centrar en el desarrollo de la aplicación. Desarrolle la aplicación con el
idioma predeterminado y prepárela para el proceso de localización sin necesidad de crear primero un archivo
de recursos predeterminado. También puede seguir el método tradicional y proporcionar una clave para
recuperar la cadena de idioma predeterminado. El nuevo flujo de trabajo, que carece de archivo .resx de
idioma predeterminado y simplemente ajusta los literales de cadena, puede ahorrar a muchos desarrolladores
la sobrecarga de localizar una aplicación. Otros desarrolladores preferirán el flujo de trabajo tradicional, ya
que facilita el trabajo con literales de cadena más largos, así como la actualización de las cadenas localizadas.
Use la implementación de IHtmlLocalizer<T> para los recursos que contienen HTML. El HTML de
IHtmlLocalizer codifica los argumentos a los que se da formato en la cadena de recursos, pero no codifica
como HTML la cadena de recursos en sí misma. En el ejemplo que se muestra a continuación, solo está
codificado en HTML el valor del parámetro name .
using System;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Localization;

namespace Localization.StarterWeb.Controllers
{
public class BookController : Controller
{
private readonly IHtmlLocalizer<BookController> _localizer;

public BookController(IHtmlLocalizer<BookController> localizer)


{
_localizer = localizer;
}

public IActionResult Hello(string name)


{
ViewData["Message"] = _localizer["<b>Hello</b><i> {0}</i>", name];

return View();
}

Nota: Generalmente solo le interesa localizar texto, no HTML.


En el nivel más bajo, puede obtener IStringLocalizerFactory de la inserción de dependencias:

{
public class TestController : Controller
{
private readonly IStringLocalizer _localizer;
private readonly IStringLocalizer _localizer2;

public TestController(IStringLocalizerFactory factory)


{
var type = typeof(SharedResource);
var assemblyName = new AssemblyName(type.GetTypeInfo().Assembly.FullName);
_localizer = factory.Create(type);
_localizer2 = factory.Create("SharedResource", assemblyName.Name);
}

public IActionResult About()


{
ViewData["Message"] = _localizer["Your application description page."]
+ " loc 2: " + _localizer2["Your application description page."];

En el código anterior se muestran los dos métodos factory.Create.


Puede dividir las cadenas localizadas por controlador o por área, o bien tener un solo contenedor. En la
aplicación de ejemplo, se usa una clase ficticia denominada SharedResource para los recursos compartidos.

// Dummy class to group shared resources

namespace Localization.StarterWeb
{
public class SharedResource
{
}
}
Algunos programadores usan la clase Startup para contener cadenas globales o compartidas. En el ejemplo
siguiente, se usan los localizadores InfoController y SharedResource :

public class InfoController : Controller


{
private readonly IStringLocalizer<InfoController> _localizer;
private readonly IStringLocalizer<SharedResource> _sharedLocalizer;

public InfoController(IStringLocalizer<InfoController> localizer,


IStringLocalizer<SharedResource> sharedLocalizer)
{
_localizer = localizer;
_sharedLocalizer = sharedLocalizer;
}

public string TestLoc()


{
string msg = "Shared resx: " + _sharedLocalizer["Hello!"] +
" Info resx " + _localizer["Hello!"];
return msg;
}

Localización de vista
El servicio IViewLocalizer proporciona cadenas localizadas para una vista. La clase ViewLocalizer
implementa esta interfaz y busca la ubicación del recurso en la ruta de acceso del archivo de vista. En el código
siguiente se muestra cómo usar la implementación predeterminada de IViewLocalizer :

@using Microsoft.AspNetCore.Mvc.Localization

@inject IViewLocalizer Localizer

@{
ViewData["Title"] = Localizer["About"];
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>

<p>@Localizer["Use this area to provide additional information."]</p>

La implementación predeterminada de IViewLocalizer busca el archivo de recursos según el nombre del


archivo de vista. No se puede usar un archivo de recursos compartidos global. ViewLocalizer implementa el
localizador mediante IHtmlLocalizer , por lo que Razor no codifica como HTML la cadena localizada. Puede
parametrizar las cadenas de recursos y IViewLocalizer codificará como HTML los parámetros, pero no la
cadena de recursos. Observe el siguiente marcado de Razor:

@Localizer["<i>Hello</i> <b>{0}!</b>", UserManager.GetUserName(User)]

Un archivo de recursos en francés podría contener lo siguiente:

KEY VALOR

<i>Hello</i> <b>{0}!</b> <i>Bonjour</i> <b>{0} !</b>

La vista representada contendría el marcado HTML del archivo de recursos.


Nota: Generalmente solo le interesa localizar texto, no HTML.
Para usar un archivo de recursos compartido en una vista, inserte IHtmlLocalizer<T> :

@using Microsoft.AspNetCore.Mvc.Localization
@using Localization.StarterWeb.Services

@inject IViewLocalizer Localizer


@inject IHtmlLocalizer<SharedResource> SharedLocalizer

@{
ViewData["Title"] = Localizer["About"];
}
<h2>@ViewData["Title"].</h2>

<h1>@SharedLocalizer["Hello!"]</h1>

Localización de DataAnnotations
Los mensajes de error de DataAnnotations se localizan con IStringLocalizer<T> . Mediante la opción
ResourcesPath = "Resources" , es posible almacenar los mensajes de error en RegisterViewModel en cualquiera
de las rutas de acceso siguientes:
Resources/ViewModels.Account.RegisterViewModel.fr.resx
Resources/ViewModels/Account/RegisterViewModel.fr.resx

public class RegisterViewModel


{
[Required(ErrorMessage = "The Email field is required.")]
[EmailAddress(ErrorMessage = "The Email field is not a valid email address.")]
[Display(Name = "Email")]
public string Email { get; set; }

[Required(ErrorMessage = "The Password field is required.")]


[StringLength(8, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
[DataType(DataType.Password)]
[Display(Name = "Password")]
public string Password { get; set; }

[DataType(DataType.Password)]
[Display(Name = "Confirm password")]
[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
public string ConfirmPassword { get; set; }
}

En ASP.NET Core MVC 1.1.0 y versiones posteriores, los atributos que no son de validación están localizados.
ASP.NET Core MVC 1.0 no busca cadenas localizadas para los atributos que no son de validación.
Uso de una cadena de recursos para varias clases
En el código siguiente se muestra cómo usar una cadena de recursos para atributos de validación con varias
clases:

public void ConfigureServices(IServiceCollection services)


{
services.AddMvc()
.AddDataAnnotationsLocalization(options => {
options.DataAnnotationLocalizerProvider = (type, factory) =>
factory.Create(typeof(SharedResource));
});
}
En el código anterior, SharedResource es la clase correspondiente al archivo resx donde se almacenan los
mensajes de validación. Según este método, DataAnnotations solo usará SharedResource , en lugar del recurso
de cada clase.

Proporcionar recursos localizados para los idiomas y las referencias


culturales admitidos
SupportedCultures y SupportedUICultures
ASP.NET Core permite especificar dos valores de referencia cultural, SupportedCultures y
SupportedUICultures . El objeto CultureInfo para SupportedCultures determina los resultados de funciones
dependientes de la referencia cultural, como el formato de fecha, hora, número y moneda. SupportedCultures
también determina el criterio de ordenación del texto, las convenciones sobre el uso de mayúsculas y
minúsculas, y las comparaciones de cadenas. Vea CultureInfo.CurrentCulture para obtener más información
sobre la manera en que el servidor obtiene la referencia cultural. SupportedUICultures determina qué cadenas
traducidas buscará ResourceManager (en archivos .resx). ResourceManager simplemente busca cadenas
específicas de referencias culturales determinadas por CurrentUICulture . Todos los subprocesos de .NET
tienen objetos CurrentCulture y CurrentUICulture . ASP.NET Core inspecciona estos valores al representar
funciones dependientes de la referencia cultural. Por ejemplo, si la referencia cultural del subproceso actual
está establecida en "en-US" (inglés (Estados Unidos)), DateTime.Now.ToLongDateString() mostrará "Thursday,
February 18, 2016". En cambio, si CurrentCulture está establecido en "es-ES" (español (España)), la salida
será "jueves, 18 de febrero de 2016".

Archivos de recursos
Un archivo de recursos es un mecanismo útil para separar del código las cadenas localizables. Las cadenas
traducidas para el idioma no predeterminado son archivos de recursos .resx aislados. Pongamos por caso que
quiere crear un archivo de recursos de español denominado Welcome.es.resx que contenga cadenas
traducidas. "es" es el código de idioma para español. Para crear este archivo de recursos en Visual Studio:
1. En el Explorador de soluciones, haga clic con el botón derecho en la carpeta que contendrá el archivo
de recursos > Agregar > Nuevo elemento.
2. En el cuadro para buscar plantillas instaladas, escriba "recurso" y asigne un nombre al archivo.

3. Escriba el valor de clave (cadena nativa) en la columna Nombre y la cadena traducida en la columna
Valor.
El archivo Welcome.es.resx aparece en Visual Studio.

Nomenclatura de los archivos de recursos


El nombre de los recursos es el nombre del tipo completo de su clase menos el nombre del ensamblado. Por
ejemplo, un recurso francés de un proyecto cuyo ensamblado principal sea LocalizationWebsite.Web.dll para
la clase LocalizationWebsite.Web.Startup se denominará Startup.fr.resx. Un recurso para la clase
LocalizationWebsite.Web.Controllers.HomeController se denominará Controllers.HomeController.fr.resx. Si el
espacio de nombres de la clase de destino no es igual que el nombre del ensamblado, necesitará el nombre de
tipo completo. Por ejemplo, en el proyecto de ejemplo, un recurso para el tipo ExtraNamespace.Tools se
denominará ExtraNamespace.Tools.fr.resx.
En el proyecto de ejemplo, el método ConfigureServices establece ResourcesPath en "Resources", por lo que
la ruta de acceso relativa del proyecto para el archivo de recursos en francés del controlador de inicio es
Resources/Controllers.HomeController.fr.resx. También puede usar carpetas para organizar los archivos de
recursos. Para el controlador de inicio, la ruta de acceso sería Resources/Controllers/HomeController.fr.resx. Si
no usa la opción ResourcesPath , el archivo .resx estará en el directorio base del proyecto. El archivo de
recursos para HomeController se denominará Controllers.HomeController.fr.resx. La opción de usar la
convención de nomenclatura de punto o ruta de acceso depende de la manera en que quiera organizar los
archivos de recursos.
NOMBRE DEL RECURSO NOMENCLATURA DE PUNTO O RUTA DE ACCESO

Resources/Controllers.HomeController.fr.resx Punto

Resources/Controllers/HomeController.fr.resx Ruta de acceso

Los archivos de recursos que usan @inject IViewLocalizer en las vistas de Razor siguen un patrón similar.
Para asignar un nombre al archivo de recursos de una vista, se puede usar la nomenclatura de punto o de ruta
de acceso. Los archivos de recursos de la vista de Razor imitan la ruta de acceso de su archivo de vista
asociado. Supongamos que establecemos ResourcesPath en "Resources". En ese caso, el archivo de recursos
de francés asociado a la vista Views/Home/About.cshtml podría ser uno de los siguientes:
Resources/Views/Home/About.fr.resx
Resources/Views.Home.About.fr.resx
Si no usa la opción ResourcesPath , el archivo .resx de una vista se ubicará en la misma carpeta que la vista.

Comportamiento de reserva de la referencia cultural


Cuando se busca un recurso, la localización entra en un proceso conocido como "reserva de la referencia
cultural". Partiendo de la referencia cultural solicitada, si esta no se encuentra, se revierte a su referencia
cultural principal correspondiente. Como inciso, decir que la propiedad CultureInfo.Parent representa la
referencia cultural principal. Eso suele conllevar (aunque no siempre) la eliminación del significante nacional
del código ISO. Así, por ejemplo, el dialecto de español hablado en México es "es-MX". Contiene el elemento
principal "es", que corresponde a español no específico de ningún país en concreto.
Imagine que su sitio recibe una solicitud sobre un recurso "Welcome" donde se emplea la referencia cultural
"fr-CA". El sistema de localización busca los recursos siguientes (en orden) y selecciona la primera
coincidencia:
Welcome.fr-CA.resx
Welcome.fr.resx
Welcome.resx (si NeutralResourcesLanguage es "fr-CA")

Consideremos, por ejemplo, que quita el designador de referencia cultural ".fr" y la referencia cultural está
establecida en francés. En ese caso, se lee el archivo de recursos predeterminado y se localizan las cadenas. El
Administrador de recursos designa un recurso predeterminado o de reserva para los casos en que nada
coincida con la referencia cultural solicitada. Si quiere simplemente devolver la clave cuando falte un recurso
para la referencia cultural solicitada, no debe tener un archivo de recursos predeterminado.
Generar archivos de recursos con Visual Studio
Si crea un archivo de recursos en Visual Studio sin una referencia cultural en el nombre de archivo (por
ejemplo, Welcome.resx), Visual Studio creará una clase de C# con una propiedad para cada cadena.
Normalmente esto no interesa con ASP.NET Core; por lo general, no tendrá un archivo de recursos .resx
predeterminado (un archivo .resx sin el nombre de la referencia cultural). Se recomienda que cree el archivo
.resx con un nombre de referencia cultural (por ejemplo, Welcome.fr.resx). Cuando cree un archivo .resx con un
nombre de referencia cultural, Visual Studio no generará el archivo de clase. Suponemos que muchos
desarrolladores no crearán un archivo de recursos de idioma predeterminado.
Agregar otras referencias culturales
Cada combinación de idioma y referencia cultural (que no sea el idioma predeterminado) requiere un archivo
de recursos único. Para crear archivos de recursos para otras referencias culturales y configuraciones
regionales, debe crear archivos de recursos en los que los códigos de idioma ISO formen parte del nombre de
archivo (por ejemplo, en-us, fr-ca y en-gb). Estos códigos ISO se colocan entre el nombre de archivo y la
extensión de archivo .resx, como en Welcome.es-MX.resx (español [México]).

Implementar una estrategia para seleccionar el idioma o la referencia


cultural de cada solicitud
Configurar la localización
La localización se configura en el método ConfigureServices :

services.AddLocalization(options => options.ResourcesPath = "Resources");

services.AddMvc()
.AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix)
.AddDataAnnotationsLocalization();

AddLocalizationagrega los servicios de localización al contenedor de servicios. El código anterior


también establece la ruta de acceso a los recursos en "Resources".
AddViewLocalization agrega compatibilidad con los archivos de vista localizados. En este ejemplo, la
localización de vista se basa en el sufijo del archivo de vista. Por ejemplo, "fr" en el archivo
Index.fr.cshtml.
AddDataAnnotationsLocalization agrega compatibilidad con mensajes de validación de DataAnnotations
localizados mediante abstracciones IStringLocalizer .
Software intermedio de localización
La referencia cultural actual de una solicitud se establece en el software intermedio de localización. El software
intermedio de localización se habilita en el método Configure . El software intermedio de localización debe
configurarse antes que cualquier software intermedio que pueda comprobar la referencia cultural de la
solicitud (por ejemplo, app.UseMvcWithDefaultRoute() ).
var supportedCultures = new[]
{
new CultureInfo(enUSCulture),
new CultureInfo("en-AU"),
new CultureInfo("en-GB"),
new CultureInfo("en"),
new CultureInfo("es-ES"),
new CultureInfo("es-MX"),
new CultureInfo("es"),
new CultureInfo("fr-FR"),
new CultureInfo("fr"),
};

app.UseRequestLocalization(new RequestLocalizationOptions
{
DefaultRequestCulture = new RequestCulture(enUSCulture),
// Formatting numbers, dates, etc.
SupportedCultures = supportedCultures,
// UI strings that we have localized.
SupportedUICultures = supportedCultures
});

app.UseStaticFiles();
// To configure external authentication,
// see: http://go.microsoft.com/fwlink/?LinkID=532715
app.UseAuthentication();
app.UseMvcWithDefaultRoute();

UseRequestLocalization inicializa un objeto RequestLocalizationOptions . En todas las solicitudes, se enumera


la lista de RequestCultureProvider en RequestLocalizationOptions y se usa el primer proveedor que puede
determinar correctamente la referencia cultural de la solicitud. Los proveedores predeterminados proceden de
la clase RequestLocalizationOptions :
1. QueryStringRequestCultureProvider
2. CookieRequestCultureProvider
3. AcceptLanguageHeaderRequestCultureProvider

La lista predeterminada va de más específico a menos específico. Más adelante en el artículo veremos cómo
puede cambiar el orden e incluso agregar un proveedor de referencia cultural personalizado. Si ninguno de los
proveedores puede determinar la referencia cultural de la solicitud, se usa DefaultRequestCulture .
QueryStringRequestCultureProvider
Algunas aplicaciones usarán una cadena de consulta para establecer la referencia cultural y la referencia
cultural de la interfaz de usuario. Para las aplicaciones que usan el método de cookie o de encabezado Accept-
Language, es útil agregar una cadena de consulta a la dirección URL para depurar y probar el código. De
forma predeterminada, QueryStringRequestCultureProvider está registrado como primer proveedor de
localización en la lista RequestCultureProvider . Debe pasar los parámetros de cadena de consulta culture y
ui-culture . En el ejemplo siguiente, la referencia cultural específica (idioma y región) se establece en español
(México):
http://localhost:5000/?culture=es-MX&ui-culture=es-MX

Si solo pasa uno de los dos parámetros ( culture o ui-culture ), el proveedor de la cadena de consulta usará
el valor que usted ha pasado para establecer ambos valores. Por ejemplo, si solo establece la referencia
cultural, se establecerán Culture y UICulture :
http://localhost:5000/?culture=es-MX

CookieRequestCultureProvider
Las aplicaciones de producción suelen proporcionar un mecanismo para establecer la referencia cultural con la
cookie de la referencia cultural de ASP.NET Core. Use el método MakeCookieValue para crear una cookie.
CookieRequestCultureProvider DefaultCookieName devuelve el nombre de cookie predeterminado usado para
realizar un seguimiento de la información de la referencia cultural preferida del usuario. El nombre de cookie
predeterminado es .AspNetCore.Culture .
El formato de la cookie es c=%LANGCODE%|uic=%LANGCODE% , donde c es Culture y uic es UICulture , por
ejemplo:

c=en-UK|uic=en-US

Si solo especifica uno de los dos valores, ya sea la información de la referencia cultural o la referencia cultural
de la interfaz de usuario, la referencia cultural especificada se usará tanto para la información de la referencia
cultural como para la referencia cultural de la interfaz de usuario.
Encabezado HTTP Accept-Language
El encabezado Accept-Language se puede establecer en la mayoría de los exploradores y está diseñado
inicialmente para especificar el idioma del usuario. Este valor indica qué debe enviar el explorador o qué ha
heredado del sistema operativo subyacente. El encabezado HTTP Accept-Language de una solicitud del
explorador no es un método infalible para detectar el idioma preferido del usuario (vea Setting language
preferences in a browser [Establecer las preferencias de idioma en un explorador]). Una aplicación de
producción debe ofrecer al usuario una manera de personalizar su opción de referencia cultural.
Establecer el encabezado HTTP Accept-Language en Internet Explorer
1. En el icono de engranaje, pulse Opciones de Internet.
2. Haga clic en Lenguajes.

3. Haga clic en Establecer preferencias de idioma.


4. Haga clic en Agregar un idioma.
5. Agregue el idioma.
6. Haga clic en el idioma y, después, en Subir.
Usar un proveedor personalizado
Imagine que quiere permitir que los clientes almacenen su idioma y su referencia cultural en las bases de
datos. Puede escribir un proveedor para que busque estos valores para el usuario. En el código siguiente se
muestra cómo agregar un proveedor personalizado:

private const string enUSCulture = "en-US";

services.Configure<RequestLocalizationOptions>(options =>
{
var supportedCultures = new[]
{
new CultureInfo(enUSCulture),
new CultureInfo("fr")
};

options.DefaultRequestCulture = new RequestCulture(culture: enUSCulture, uiCulture: enUSCulture);


options.SupportedCultures = supportedCultures;
options.SupportedUICultures = supportedCultures;

options.RequestCultureProviders.Insert(0, new CustomRequestCultureProvider(async context =>


{
// My custom request culture logic
return new ProviderCultureResult("en");
}));
});

Use RequestLocalizationOptions para agregar o quitar proveedores de localización.


Establecer la referencia cultural mediante programación
Este proyecto Localization.StarterWeb de ejemplo de GitHub contiene una interfaz de usuario para
establecer el valor Culture . El archivo Views/Shared/_SelectLanguagePartial.cshtml le permite seleccionar la
referencia cultural de la lista de referencias culturales admitidas:
@using Microsoft.AspNetCore.Builder
@using Microsoft.AspNetCore.Http.Features
@using Microsoft.AspNetCore.Localization
@using Microsoft.AspNetCore.Mvc.Localization
@using Microsoft.Extensions.Options

@inject IViewLocalizer Localizer


@inject IOptions<RequestLocalizationOptions> LocOptions

@{
var requestCulture = Context.Features.Get<IRequestCultureFeature>();
var cultureItems = LocOptions.Value.SupportedUICultures
.Select(c => new SelectListItem { Value = c.Name, Text = c.DisplayName })
.ToList();
var returnUrl = string.IsNullOrEmpty(Context.Request.Path) ? "~/" : $"~{Context.Request.Path.Value}";
}

<div title="@Localizer["Request culture provider:"] @requestCulture?.Provider?.GetType().Name">


<form id="selectLanguage" asp-controller="Home"
asp-action="SetLanguage" asp-route-returnUrl="@returnUrl"
method="post" class="form-horizontal" role="form">
<label asp-for="@requestCulture.RequestCulture.UICulture.Name">@Localizer["Language:"]</label>
<select name="culture"
onchange="this.form.submit();"
asp-for="@requestCulture.RequestCulture.UICulture.Name" asp-items="cultureItems">
</select>
</form>
</div>

El archivo Views/Shared/_SelectLanguagePartial.cshtml se agrega a la sección footer del archivo de diseño


para que esté disponible para todas las vistas:

<div class="container body-content" style="margin-top:60px">


@RenderBody()
<hr>
<footer>
<div class="row">
<div class="col-md-6">
<p>&copy; @System.DateTime.Now.Year - Localization.StarterWeb</p>
</div>
<div class="col-md-6 text-right">
@await Html.PartialAsync("_SelectLanguagePartial")
</div>
</div>
</footer>
</div>

El método SetLanguage establece la cookie de la referencia cultural.

[HttpPost]
public IActionResult SetLanguage(string culture, string returnUrl)
{
Response.Cookies.Append(
CookieRequestCultureProvider.DefaultCookieName,
CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)),
new CookieOptions { Expires = DateTimeOffset.UtcNow.AddYears(1) }
);

return LocalRedirect(returnUrl);
}

No se puede conectar el archivo _SelectLanguagePartial.cshtml con código de ejemplo para este proyecto. El
proyecto Localization.StarterWeb de GitHub tiene código para hacer fluir RequestLocalizationOptions a una
vista parcial de Razor a través del contenedor de inserción de dependencias.

Términos relacionados con la globalización y la localización


Para localizar una aplicación también es necesario contar con unos conocimientos básicos sobre los juegos de
caracteres pertinentes que se usan en el desarrollo de software moderno y sobre los problemas asociados.
Aunque todos los equipos almacenan texto como números (códigos), cada sistema almacena el mismo texto
con números diferentes. El proceso de localización consiste en traducir la interfaz de usuario (IU ) de la
aplicación a una referencia cultural o configuración regional específica.
La localizabilidad es un proceso intermedio para comprobar que una aplicación globalizada está preparada
para la localización.
El formato RFC 4646 para el nombre de la referencia cultural es <languagecode2>-<country/regioncode2> ,
donde <languagecode2> es el código de idioma y <country/regioncode2> es el código de la referencia cultural
secundaria. Por ejemplo, es-CL para español (Chile), en-US para inglés (Estados Unidos) y en-AU para inglés
(Australia). RFC 4646 es una combinación de un código de referencia cultural ISO 639 de dos letras en
minúsculas asociado con un idioma y un código de referencia cultural secundaria ISO 3166 de dos letras en
mayúsculas asociado con un país o región. Vea Language Culture Name (Nombre de la referencia cultural del
idioma).
"Internacionalización" suele abreviarse como "I18N". La abreviatura toma la primera y la última letra de la
palabra, y el número de letras que hay entre ellas, es decir, el 18 representa el número de letras que hay entre
la "I" inicial y la "N" final. Lo mismo se puede decir de "globalización" (G11N ) y "localización" (L10N ).
Términos:
Globalización (G11N ): es el proceso de hacer que una aplicación admita diferentes idiomas y regiones.
Localización (L10N ): es el proceso de personalizar una aplicación para un idioma y región determinados.
Internacionalización (I18N ): hace referencia a la globalización y la localización.
Referencia cultural: es un idioma y, opcionalmente, una región.
Referencia cultural neutra: se trata de una referencia cultural que tiene un idioma especificado, pero no una
región (por ejemplo, "en" y "es").
Referencia cultural específica: es una referencia cultural que tiene un idioma y una región especificados
(por ejemplo, "en-US", "en-GB" y "es-CL").
Referencia cultural principal: se trata de la referencia cultural neutra que contiene una referencia cultural
específica (por ejemplo, "en" es la referencia cultural principal de "en-US" y "en-GB").
Configuración regional: la configuración regional es lo mismo que la referencia cultural.

Recursos adicionales
Proyecto Localization.StarterWeb usado en el artículo
Archivos de recursos en Visual Studio
Recursos en archivos .resx
Kit de herramientas de aplicaciones multilingüe de Microsoft
Configurar la localización de objetos portátiles en
ASP.NET Core
25/06/2018 • 12 minutes to read • Edit Online

Por Sébastien Ros y Scott Addie


En este artículo se describen los pasos para usar archivos de objeto portátil (PO ) en una aplicación ASP.NET Core
con el marco de trabajo de Orchard Core.
Nota: Orchard Core no es un producto de Microsoft. Por tanto, Microsoft no ofrece soporte técnico para esta
característica.
Vea o descargue el código de ejemplo (cómo descargarlo)

¿Qué es un archivo de objeto portátil?


Los archivos de objeto portátil se distribuyen como archivos de texto que contienen las cadenas traducidas de un
idioma determinado. Entre las ventajas de usar archivos de objeto portátil en lugar de archivos .resx se incluyen las
siguientes:
Los archivos de objeto portátil admiten la pluralización, a diferencia de los archivos .resx.
Los archivos de objeto portátil no se compilan como archivos .resx. Por tanto, no se requieren herramientas ni
pasos de compilación especializados.
Los archivos de objeto portátil funcionan bien con herramientas de colaboración de edición en línea.
Ejemplo
Este es un archivo de objeto portátil de ejemplo que contiene la traducción de dos cadenas en francés, incluida una
con su forma en plural:
fr.po

#: Services/EmailService.cs:29
msgid "Enter a comma separated list of email addresses."
msgstr "Entrez une liste d'emails séparés par une virgule."

#: Views/Email.cshtml:112
msgid "The email address is \"{0}\"."
msgid_plural "The email addresses are \"{0}\"."
msgstr[0] "L'adresse email est \"{0}\"."
msgstr[1] "Les adresses email sont \"{0}\""

En este ejemplo se usa la sintaxis siguiente:


#: : comentario que indica el contexto de la cadena que se va a traducir. La misma cadena se podría traducir de
forma diferente según donde se vaya a usar.
msgid : la cadena sin traducir.
msgstr : la cadena traducida.

En el caso de compatibilidad con la pluralización, se pueden definir más entradas.


msgid_plural : la cadena en plural sin traducir.
msgstr[0] : la cadena traducida para el caso 0.
msgstr[N] : la cadena traducida para el caso N.
Encontrará aquí la especificación del archivo de objeto portátil.

Configuración de la compatibilidad con archivos de objeto portátil en


ASP.NET Core
Este ejemplo se basa en una aplicación ASP.NET Core MVC generada a partir de una plantilla de proyecto de
Visual Studio 2017.
Hacer referencia al paquete
Agregue una referencia al paquete NuGet OrchardCore.Localization.Core . Este paquete se encuentra disponible en
MyGet, en el siguiente origen de paquete: https://www.myget.org/F/orchardcore-preview/api/v3/index.json.
El archivo .csproj ahora contiene una línea similar a la siguiente (el número de versión puede variar):

<PackageReference Include="OrchardCore.Localization.Core" Version="1.0.0-beta1-3187" />

Registrar el servicio
Agregue los servicios necesarios al método ConfigureServices de Startup.cs:

public void ConfigureServices(IServiceCollection services)


{
services.AddMvc()
.AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix);

services.AddPortableObjectLocalization();

services.Configure<RequestLocalizationOptions>(options =>
{
var supportedCultures = new List<CultureInfo>
{
new CultureInfo("en-US"),
new CultureInfo("en"),
new CultureInfo("fr-FR"),
new CultureInfo("fr")
};

options.DefaultRequestCulture = new RequestCulture("en-US");


options.SupportedCultures = supportedCultures;
options.SupportedUICultures = supportedCultures;
});
}

Agregue el software intermedio necesario al método Configure de Startup.cs:


public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}

app.UseStaticFiles();

app.UseRequestLocalization();

app.UseMvcWithDefaultRoute();
}

Agregue el código siguiente a la vista de Razor de su elección. En este ejemplo se usa About.cshtml.

@using Microsoft.AspNetCore.Mvc.Localization
@inject IViewLocalizer Localizer

<p>@Localizer["Hello world!"]</p>

Se inserta una instancia de IViewLocalizer que se usa para traducir el texto "Hello world!".
Crear un archivo de objeto portátil
Cree un archivo denominado .po en la carpeta raíz de la aplicación. En este ejemplo, el nombre del archivo es fr.po
porque se usa el idioma francés:

msgid "Hello world!"


msgstr "Bonjour le monde!"

En este archivo se almacenan la cadena que se va a traducir y la cadena traducida al francés. Si es necesario, las
traducciones se revierten a su referencia cultural principal. En este ejemplo, el archivo fr.po se usa si la referencia
cultural solicitada es fr-FR o fr-CA .
Probar la aplicación
Ejecute la aplicación y vaya a la URL /Home/About . El texto Hello world! aparece en pantalla.
Vaya a la dirección URL /Home/About?culture=fr-FR . El texto Bonjour le monde! aparece en pantalla.

Pluralización
Los archivos de objeto portátil son compatibles con las formas de pluralización, lo que resulta útil cuando es
necesario traducir la misma cadena de forma diferente en función de una cardinalidad. Esta tarea se ve dificultada
por el hecho de que cada idioma define reglas personalizadas para seleccionar qué cadena se va a usar en función
de la cardinalidad.
El paquete de localización de Orchard proporciona una API para invocar automáticamente estas diferentes formas
plurales.
Crear archivos de objeto portátil de pluralización
Agregue el contenido siguiente al archivo fr.po mencionado anteriormente:
msgid "There is one item."
msgid_plural "There are {0} items."
msgstr[0] "Il y a un élément."
msgstr[1] "Il y a {0} éléments."

Vea ¿Qué es un archivo de objeto portátil? para saber qué representa cada entrada de este ejemplo.
Agregar un idioma con formas de pluralización diferentes
En el ejemplo anterior se han usado cadenas en inglés y francés. El idioma inglés y francés solo tienen dos formas
de pluralización y comparten las mismas reglas de formato, es decir, se asigna una cardinalidad de uno a la primera
forma plural. Todas las demás cardinalidades se asignan a la segunda forma plural.
No todos los idiomas comparten las mismas reglas. Esto se muestra con el idioma checo, que tiene tres formas
plurales.
Cree el archivo cs.po como se indica a continuación y observe que la pluralización requiere tres traducciones
diferentes:

msgid "Hello world!"


msgstr "Ahoj světe!!"

msgid "There is one item."


msgid_plural "There are {0} items."
msgstr[0] "Existuje jedna položka."
msgstr[1] "Existují {0} položky."
msgstr[2] "Existuje {0} položek."

Para aceptar localizaciones en checo, agregue "cs" a la lista de referencias culturales admitidas en el método
ConfigureServices :

var supportedCultures = new List<CultureInfo>


{
new CultureInfo("en-US"),
new CultureInfo("en"),
new CultureInfo("fr-FR"),
new CultureInfo("fr"),
new CultureInfo("cs")
};

Edite el archivo Views/Home/About.cshtml para representar cadenas localizadas en plural para diversas
cardinalidades:

<p>@Localizer.Plural(1, "There is one item.", "There are {0} items.")</p>


<p>@Localizer.Plural(2, "There is one item.", "There are {0} items.")</p>
<p>@Localizer.Plural(5, "There is one item.", "There are {0} items.")</p>

Nota: En un escenario real, se usaría una variable para representar el número. En este caso, repetimos el mismo
código con tres valores diferentes para exponer un caso muy específico.
Si cambia las referencias culturales, verá lo siguiente:
Para /Home/About :

There is one item.


There are 2 items.
There are 5 items.
Para /Home/About?culture=fr :

Il y a un élément.
Il y a 2 éléments.
Il y a 5 éléments.

Para /Home/About?culture=cs :

Existuje jedna položka.


Existují 2 položky.
Existuje 5 položek.

Tenga en cuenta que para la referencia cultural de checo, las tres traducciones son diferentes. Las referencias
culturales de inglés y francés comparten la misma construcción para las dos últimas cadenas traducidas.

Tareas avanzadas
Contextualización de cadenas
Las aplicaciones a menudo contienen las cadenas que se van a traducir en lugares diferentes. La misma cadena
puede tener una traducción diferente en determinadas ubicaciones dentro de una aplicación (vistas de Razor o
archivos de clase). Un archivo de objeto portátil admite el concepto de contexto de archivo, que se puede usar para
clasificar la cadena que se va a representar. Mediante el uso de un contexto de archivo, una cadena se puede
traducir de forma diferente según el contexto de archivo (o según la falta de contexto de archivo).
Los servicios de localización de objetos portátiles usan el nombre de la clase completa o la vista que se usa al
traducir una cadena. Esto se logra mediante el establecimiento del valor en la entrada msgctxt .
Considere la posibilidad de realizar una adición mínima al ejemplo fr.po anterior. Una vista de Razor ubicada en
Views/Home/About.cshtml se puede definir como el contexto de archivo si se establece el valor de entrada
reservado msgctxt :

msgctxt "Views.Home.About"
msgid "Hello world!"
msgstr "Bonjour le monde!"

Después de establecer msgctxt , el texto se traduce cuando se va a /Home/About?culture=fr-FR . La traducción no se


llevará a cabo al ir a /Home/Contact?culture=fr-FR .
Cuando ninguna entrada específica coincide con un contexto de archivo determinado, el mecanismo de reserva de
Orchard Core busca un archivo de objeto portátil adecuado sin contexto. Suponiendo que no haya ningún contexto
de archivo específico definido para Views/Home/Contact.cshtml, al ir a /Home/Contact?culture=fr-FR se carga un
archivo de objeto portátil como:

msgid "Hello world!"


msgstr "Bonjour le monde!"

Cambiar la ubicación de los archivos de objeto portátil


La ubicación predeterminada de los archivos de objeto portátil se puede cambiar en ConfigureServices :

services.AddPortableObjectLocalization(options => options.ResourcesPath = "Localization");

En este ejemplo, los archivos de objeto portátil se cargan desde la carpeta Localization.
Implementar una lógica personalizada para buscar archivos de localización
Cuando se necesita una lógica más compleja para buscar archivos de objeto portátil, es posible implementar y
registrar la interfaz OrchardCore.Localization.PortableObject.ILocalizationFileLocationProvider como un servicio.
Esto es útil cuando los archivos de objeto portátil se pueden almacenar en ubicaciones diferentes o cuando los
archivos deben encontrarse en una jerarquía de carpetas.
Usar un idioma pluralizado predeterminado diferente
El paquete incluye un método de extensión Plural que es específico para dos formas plurales. Para los idiomas
que requieren más formas plurales, es necesario crear un método de extensión. Con un método de extensión, no
tendrá que proporcionar ningún archivo de localización para el idioma predeterminado, dado que las cadenas
originales ya están disponibles directamente en el código.
Puede usar la sobrecarga Plural(int count, string[] pluralForms, params object[] arguments) más genérica, que
acepta una matriz de cadenas de traducciones.
Inicio de solicitudes HTTP
19/06/2018 • 23 minutes to read • Edit Online

Por Glenn Condron, Ryan Nowak y Steve Gordon


Se puede registrar y usar un IHttpClientFactory para crear y configurar instancias de HttpClient en una
aplicación. Esto reporta las siguientes ventajas:
Proporciona una ubicación central para denominar y configurar instancias de HttpClient lógicas. Así, por
ejemplo, se podría registrar y configurar un cliente "github" para tener acceso a GitHub y, de igual modo,
registrar otro cliente predeterminado para otros fines.
Codifica el concepto de middleware saliente a través de controladores de delegación en HttpClient y
proporciona extensiones para middleware basado en Polly para poder sacar partido de este.
Administra la agrupación y duración de las instancias de HttpClientMessageHandler subyacentes para evitar los
problemas de DNS que suelen producirse al administrar las duraciones de HttpClient manualmente.
Agrega una experiencia de registro configurable (a través de ILogger ) en todas las solicitudes enviadas a
través de los clientes creados por Factory.

Patrones de consumo
IHttpClientFactory se puede usar de varias formas en una aplicación:
Uso básico
Clientes con nombre
Clientes con tipo
Clientes generados
Ninguno de ellos es rigurosamente superior a otro, sino que el mejor método dependerá de las restricciones
particulares de la aplicación.
Uso básico
Se puede registrar un IHttpClientFactory llamando al método de extensión AddHttpClient en
IServiceCollection , dentro del método ConfigureServices en Startup.cs.

services.AddHttpClient();

Una vez registrado, el código puede aceptar un IHttpClientFactory en cualquier parte donde se puedan insertar
servicios por medio de la inserción de dependencias. El IHttpClientFactory se puede usar para crear una instancia
de HttpClient :
public class BasicUsageModel : PageModel
{
private readonly IHttpClientFactory _clientFactory;

public IEnumerable<GitHubBranch> Branches { get; private set; }

public bool GetBranchesError { get; private set; }

public BasicUsageModel(IHttpClientFactory clientFactory)


{
_clientFactory = clientFactory;
}

public async Task OnGet()


{
var request = new HttpRequestMessage(HttpMethod.Get,
"https://api.github.com/repos/aspnet/docs/branches");
request.Headers.Add("Accept", "application/vnd.github.v3+json");
request.Headers.Add("User-Agent", "HttpClientFactory-Sample");

var client = _clientFactory.CreateClient();

var response = await client.SendAsync(request);

if (response.IsSuccessStatusCode)
{
Branches = await response.Content.ReadAsAsync<IEnumerable<GitHubBranch>>();
}
else
{
GetBranchesError = true;
Branches = Array.Empty<GitHubBranch>();
}
}
}

Cuando IHttpClientFactory se usa de este modo, constituye una excelente manera de refactorizar una aplicación
existente. No tiene efecto alguno en la forma en que HttpClient se usa. En aquellos sitios en los que ya se hayan
creado instancias de HttpClient , reemplace esas apariciones por una llamada a CreateClient .
Clientes con nombre
Si una aplicación necesita usar HttpClient de diversas maneras, cada una con una configuración diferente, una
opción consiste en usar clientes con nombre. La configuración de un HttpClient con nombre se puede realizar
durante la fase de registro en ConfigureServices .

services.AddHttpClient("github", c =>
{
c.BaseAddress = new Uri("https://api.github.com/");
c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json"); // Github API versioning
c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample"); // Github requires a user-agent
});

En el código anterior, se llama a AddHttpClient usando el nombre "github". Este cliente tiene aplicadas algunas
configuraciones predeterminadas, a saber, la dirección base y dos encabezados necesarios para trabajar con la API
de GitHub.
Cada vez que se llama a CreateClient , se crea otra instancia de HttpClient y se llama a la acción de
configuración.
Para consumir un cliente con nombre, se puede pasar un parámetro de cadena a CreateClient . Especifique el
nombre del cliente que se va a crear:
public class NamedClientModel : PageModel
{
private readonly IHttpClientFactory _clientFactory;

public IEnumerable<GitHubPullRequest> PullRequests { get; private set; }

public bool GetPullRequestsError { get; private set; }

public bool HasPullRequests => PullRequests.Any();

public NamedClientModel(IHttpClientFactory clientFactory)


{
_clientFactory = clientFactory;
}

public async Task OnGet()


{
var request = new HttpRequestMessage(HttpMethod.Get, "repos/aspnet/docs/pulls");

var client = _clientFactory.CreateClient("github");

var response = await client.SendAsync(request);

if (response.IsSuccessStatusCode)
{
PullRequests = await response.Content.ReadAsAsync<IEnumerable<GitHubPullRequest>>();
}
else
{
GetPullRequestsError = true;
PullRequests = Array.Empty<GitHubPullRequest>();
}
}
}

En el código anterior, no es necesario especificar un nombre de host en la solicitud. Basta con pasar solo la ruta de
acceso, ya que se usa la dirección base configurada del cliente.
Clientes con tipo
Los clientes con tipo proporcionan las mismas funciones que los clientes con nombre sin la necesidad de usar
cadenas como claves. El método del cliente con tipo proporciona ayuda de compilador e IntelliSense al consumir
clientes. Ofrecen una sola ubicación para configurar un determinado HttpClient e interactuar con él. Por ejemplo,
el mismo cliente con tipo se puede usar para un punto de conexión back-end único y encapsular toda la lógica que
se ocupa de ese punto de conexión. Otra ventaja es que funcionan con la inserción de dependencias, de modo que
se pueden insertar cuando sea necesario en la aplicación.
Un cliente con tipo acepta un parámetro HttpClient en su constructor:
public class GitHubService
{
public HttpClient Client { get; }

public GitHubService(HttpClient client)


{
client.BaseAddress = new Uri("https://api.github.com/");
client.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json"); // GitHub API versioning
client.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample"); // GitHub requires a user-
agent

Client = client;
}

public async Task<IEnumerable<GitHubIssue>> GetAspNetDocsIssues()


{
var response = await Client.GetAsync("/repos/aspnet/docs/issues?
state=open&sort=created&direction=desc");

response.EnsureSuccessStatusCode();

var result = await response.Content.ReadAsAsync<IEnumerable<GitHubIssue>>();

return result;
}
}

En el código anterior, la configuración se mueve al cliente con tipo. El objeto HttpClient se expone como una
propiedad pública. Se pueden definir métodos específicos de API que exponen la funcionalidad HttpClient . El
método GetAspNetDocsIssues encapsula el código necesario para consultar y analizar los últimos problemas
abiertos de un repositorio de GitHub.
Para registrar un cliente con tipo, se puede usar el método de extensión genérico AddHttpClient dentro de
ConfigureServices , especificando la clase del cliente con tipo:

services.AddHttpClient<GitHubService>();

El cliente con tipo se registra como transitorio con inserción con dependencias, y se puede insertar y consumir
directamente:
public class TypedClientModel : PageModel
{
private readonly GitHubService _gitHubService;

public IEnumerable<GitHubIssue> LatestIssues { get; private set; }

public bool HasIssue => LatestIssues.Any();

public bool GetIssuesError { get; private set; }

public TypedClientModel(GitHubService gitHubService)


{
_gitHubService = gitHubService;
}

public async Task OnGet()


{
try
{
LatestIssues = await _gitHubService.GetAspNetDocsIssues();
}
catch(HttpRequestException)
{
GetIssuesError = true;
LatestIssues = Array.Empty<GitHubIssue>();
}
}
}

Si lo prefiere, la configuración de un cliente con nombre se puede especificar durante su registro en


ConfigureServices , en lugar de en su constructor:

services.AddHttpClient<RepoService>(c =>
{
c.BaseAddress = new Uri("https://api.github.com/");
c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});

El HttpClient se puede encapsular completamente dentro de un cliente con nombre. En lugar de exponerlo como
una propiedad, se pueden proporcionar métodos públicos que llamen a la instancia de HttpClient internamente.

public class RepoService


{
private readonly HttpClient _httpClient; // not exposed publicly

public RepoService(HttpClient client)


{
_httpClient = client;
}

public async Task<IEnumerable<string>> GetRepos()


{
var response = await _httpClient.GetAsync("aspnet/repos");

response.EnsureSuccessStatusCode();

var result = await response.Content.ReadAsAsync<IEnumerable<string>>();

return result;
}
}
En el código anterior, el HttpClient se almacena como un campo privado. Todo el acceso para realizar llamadas
externas pasa por el método GetRepos .
Clientes generados
IHttpClientFactory se puede usar en combinación con otras bibliotecas de terceros, como Refit. Refit es una
biblioteca de REST para .NET que convierte las API de REST en interfaces en vivo. Se genera una implementación
de la interfaz dinámicamente por medio de RestService , usando HttpClient para realizar las llamadas HTTP
externas.
Se define una interfaz y una respuesta para representar la API externa y su correspondiente respuesta:

public interface IHelloClient


{
[Get("/helloworld")]
Task<Reply> GetMessageAsync();
}

public class Reply


{
public string Message { get; set; }
}

Un cliente con tipo se puede agregar usando Refit para generar la implementación:

public void ConfigureServices(IServiceCollection services)


{
services.AddHttpClient("hello", c =>
{
c.BaseAddress = new Uri("http://localhost:5000");
})
.AddTypedClient(c => Refit.RestService.For<IHelloClient>(c));

services.AddMvc();
}

La interfaz definida se puede usar cuando sea preciso con la implementación proporcionada por la inserción de
dependencias y Refit:

[ApiController]
public class ValuesController : ControllerBase
{
private readonly IHelloClient _client;

public ValuesController(IHelloClient client)


{
_client = client;
}

[HttpGet("/")]
public async Task<ActionResult<Reply>> Index()
{
return await _client.GetMessageAsync();
}
}

Middleware de solicitud saliente


HttpClient ya posee el concepto de controladores de delegación, que se pueden vincular entre sí para las
solicitudes HTTP salientes. IHttpClientFactory permite definir fácilmente los controladores que se usarán en cada
cliente con nombre. Admite el registro y encadenamiento de varios controladores para crear una canalización de
middleware de solicitud saliente. Cada uno de estos controladores es capaz de realizar la tarea antes y después de
la solicitud de salida. Este patrón es similar a la canalización de middleware de entrada de ASP.NET Core. Dicho
patrón proporciona un mecanismo para administrar cuestiones transversales relativas a las solicitudes HTTP, como
el almacenamiento en caché, el control de errores, la serialización y el registro.
Para crear un controlador, defina una clase que se derive de DelegatingHandler . Invalide el método SendAsync
para ejecutar el código antes de pasar la solicitud al siguiente controlador de la canalización:

public class ValidateHeaderHandler : DelegatingHandler


{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
{
if (!request.Headers.Contains("X-API-KEY"))
{
return new HttpResponseMessage(HttpStatusCode.BadRequest)
{
Content = new StringContent("You must supply an API key header called X-API-KEY")
};
}

return await base.SendAsync(request, cancellationToken);


}
}

El código anterior define un controlador básico. Comprueba si se ha incluido un encabezado X-API-KEY en la


solicitud. Si no está presente, puede evitar la llamada HTTP y devolver una respuesta adecuada.
Durante el registro, se pueden agregar uno o varios controladores a la configuración de un HttpClient . Esta tarea
se realiza a través de métodos de extensión en IHttpClientBuilder .

services.AddTransient<ValidateHeaderHandler>();

services.AddHttpClient("externalservice", c =>
{
c.BaseAddress = new Uri("https://localhost:5000/"); // assume this is an "external" service which requires
an API KEY
})
.AddHttpMessageHandler<ValidateHeaderHandler>();

En el código anterior, ValidateHeaderHandler se ha registrado con inserción de dependencias. El controlador debe


estar registrado en la inserción de dependencias como transitorio. Una vez registrado, se puede llamar a
AddHttpMessageHandler , pasando el tipo del controlador.

Se pueden registrar varios controladores en el orden en que deben ejecutarse. Cada controlador contiene el
siguiente controlador hasta que el último HttpClientHandler ejecuta la solicitud:

services.AddTransient<SecureRequestHandler>();
services.AddTransient<RequestDataHandler>();

services.AddHttpClient("clientwithhandlers")
// This handler is on the outside and called first during the request, last during the response.
.AddHttpMessageHandler<SecureRequestHandler>()
// This handler is on the inside, closest to the request being sent.
.AddHttpMessageHandler<RequestDataHandler>();

Usar controladores basados en Polly


IHttpClientFactory se integra con una biblioteca de terceros muy conocida denominada Polly. Polly es una
biblioteca con capacidades de resistencia y control de errores transitorios para .NET. Permite a los desarrolladores
expresar directivas como, por ejemplo, de reintento, interruptor, tiempo de espera, aislamiento compartimentado y
reserva de forma fluida y segura para los subprocesos.
Se proporcionan métodos de extensión para hacer posible el uso de directivas de Polly con instancias de
HttpClient configuradas. Encontrará extensiones de Polly disponibles en un paquete NuGet llamado
"Microsoft.Extensions.Http.Polly". Este paquete no está incluido de forma predeterminada en el metapaquete
"Microsoft.AspNetCore.App". Para usar las extensiones, hay que incluir in elemento PackageReference
explícitamente en el proyecto.

<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.0-rc1-final" />
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="2.1.0-rc1-final" />
<PackageReference Include="Microsoft.VisualStudio.Web.BrowserLink" Version="2.1.0-rc1-final" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.1.0-rc1-final" />
</ItemGroup>

</Project>

Tras restaurar este paquete, hay métodos de extensión disponibles para admitir la adición de controladores
basados en Polly en clientes.
Control de errores transitorios
Los errores más comunes que se puede esperar que ocurran al realizar llamadas HTTP externas serán transitorios.
Por ello, se incluye un método de extensión muy práctico denominado AddTransientHttpErrorPolicy , que permite
definir una directiva para controlar los errores transitorios. Las directivas que se configuran con este método de
extensión controlan HttpRequestException , las respuestas HTTP 5xx y las respuestas HTTP 408.
La extensión AddTransientHttpErrorPolicy se puede usar en ConfigureServices . La extensión da acceso a un
objeto PolicyBuilder , configurado para controlar los errores que pueden constituir un posible error transitorio:

services.AddHttpClient<UnreliableEndpointCallerService>()
.AddTransientHttpErrorPolicy(p => p.WaitAndRetryAsync(3, _ => TimeSpan.FromMilliseconds(600)));

En el código anterior, se define una directiva WaitAndRetryAsync . Las solicitudes erróneas se reintentan hasta tres
veces con un retardo de 600 ms entre intentos.
Seleccionar directivas dinámicamente
Existen más métodos de extensión que pueden servir para agregar controladores basados en Polly. Una de esas
extensiones es AddPolicyHandler , que tiene varias sobrecargas. Una de esas sobrecargas permite inspeccionar la
solicitud al dilucidar qué directiva aplicar:

var timeout = Policy.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(10));


var longTimeout = Policy.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(30));

services.AddHttpClient("conditionalpolicy")
// Run some code to select a policy based on the request
.AddPolicyHandler(request => request.Method == HttpMethod.Get ? timeout : longTimeout);
En el código anterior, si la solicitud de salida es GET, se aplica un tiempo de espera de 10 segundos. En cualquier
otro método HTTP, se usa un tiempo de espera de 30 segundos.
Agregar varios controladores de Polly
Es habitual anidar directivas de Polly para proporcionar una mejor funcionalidad:

services.AddHttpClient("multiplepolicies")
.AddTransientHttpErrorPolicy(p => p.RetryAsync(3))
.AddTransientHttpErrorPolicy(p => p.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));

En el ejemplo anterior, se agregan dos controladores. En el primer ejemplo se usa la extensión


AddTransientHttpErrorPolicy para agregar una directiva de reintento. Las solicitudes con error se reintentan hasta
tres veces. La segunda llamada a AddTransientHttpErrorPolicy agrega una directiva de interruptor. Las solicitudes
externas subsiguientes se bloquean durante 30 segundos si se producen cinco intentos infructuosos seguidos. Las
directivas de interruptor tienen estado. Así, todas las llamadas realizadas a través de este cliente comparten el
mismo estado de circuito.
Agregar directivas desde el Registro de Polly
Una forma de administrar las directivas usadas habitualmente consiste en definirlas una vez y registrarlas con
PolicyRegistry . Se proporciona un método de extensión que permite agregar un controlador por medio de una
directiva del Registro:

var registry = services.AddPolicyRegistry();

registry.Add("regular", timeout);
registry.Add("long", longTimeout);

services.AddHttpClient("regulartimeouthandler")
.AddPolicyHandlerFromRegistry("regular");

En el código anterior, se agrega un elemento PolicyRegistry a ServiceCollection y, con él, se registran dos
directivas. Para poder usar una directiva del Registro, se usa el método AddPolicyHandlerFromRegistry , pasando el
nombre de la directiva que se va a aplicar.
Encontrará más información sobre IHttpClientFactory y las integraciones de Polly en la wiki de Polly.

HttpClient y administración de la duración


Cada vez que se llama a CreateClient en IHttpClientFactory , se devuelve una nueva instancia de HttpClient .
Habrá un HttpMessageHandler por cada cliente con nombre. IHttpClientFactory agrupará las instancias de
HttpMessageHandler creadas por Factory para reducir el consumo de recursos. Se puede reutilizar una instancia de
HttpMessageHandler del grupo al crear una instancia de HttpClient si su duración aún no ha expirado.

La agrupación de controladores es conveniente porque cada controlador suele administrar sus propias conexiones
HTTP subyacentes. Crear más controladores de lo necesario puede provocar retrasos en la conexión. Además,
algunos controladores dejan las conexiones abiertas de forma indefinida, lo que puede ser un obstáculo a la hora
de reaccionar ante los cambios de DNS.
La duración de controlador predeterminada es dos minutos. El valor predeterminado se puede reemplazar
individualmente en cada cliente con nombre. Para ello, llame a SetHandlerLifetime en el IHttpClientBuilder que
se devuelve cuando se crea el cliente:

services.AddHttpClient("extendedhandlerlifetime")
.SetHandlerLifetime(TimeSpan.FromMinutes(5));
Registro
Los clientes que se han creado a través de IHttpClientFactory registran mensajes de registro de todas las
solicitudes. Deberá habilitar el nivel de información adecuado en la configuración del registro para ver los
mensajes de registro predeterminados. El registro de más información, como el registro de encabezados de
solicitud, solo se incluye en el nivel de seguimiento.
La categoría de registro usada en cada cliente incluye el nombre del cliente. Así, por ejemplo, un cliente llamado
"MyNamedClient" registra mensajes con una categoría System.Net.Http.HttpClient.MyNamedClient.LogicalHandler .
Los mensajes con el sufijo "LogicalHandler" se producen fuera de la canalización de controlador de la solicitud. En
la solicitud, los mensajes se registran antes de que cualquier otro controlador de la canalización haya procesado la
solicitud. En la respuesta, los mensajes se registran después de que cualquier otro controlador de la canalización
haya recibido la respuesta.
El registro también se produce dentro de la canalización de controlador de la solicitud. En nuestro caso de ejemplo
de "MyNamedClient", esos mensajes se registran en la categoría de registro
System.Net.Http.HttpClient.MyNamedClient.ClientHandler . En la solicitud, esto tiene lugar después de que todos los
demás controladores se hayan ejecutado y justo antes de que la solicitud se envíe por la red. En la respuesta, este
registro incluye el estado de la respuesta antes de que vuelva a pasar por la canalización del controlador.
Si se habilita el registro tanto dentro como fuera de la canalización, se podrán inspeccionar los cambios realizados
por otros controladores de la canalización. Esto puede englobar cambios, por ejemplo, en los encabezados de
solicitud o en el código de estado de la respuesta.
Si el nombre del cliente se incluye en la categoría de registro, dicho registro se podrá filtrar para encontrar clientes
con nombre específicos cuando sea necesario.

Configurar HttpMessageHandler
Puede que sea necesario controlar la configuración del elemento HttpMessageHandler interno usado por un cliente.
Se devuelve un IHttpClientBuilder cuando se agregan clientes con nombre o con tipo. Se puede usar el método
de extensión ConfigurePrimaryHttpMessageHandler para definir un delegado. Este delegado servirá para crear y
configurar el elemento principal HttpMessageHandler que ese cliente usa:

services.AddHttpClient("configured-inner-handler")
.ConfigurePrimaryHttpMessageHandler(() =>
{
return new HttpClientHandler()
{
AllowAutoRedirect = false,
UseDefaultCredentials = true
};
});
Características de solicitud de ASP.NET Core
25/06/2018 • 5 minutes to read • Edit Online

Por Steve Smith


Los detalles de implementación del servidor web relacionados con las solicitudes HTTP y las respuestas se
definen en las interfaces. Estas interfaces se usan en implementaciones de servidor web y middleware para crear
y modificar la canalización de hospedaje de la aplicación.

Interfaces de características
ASP.NET Core define una serie de interfaces de características HTTP en Microsoft.AspNetCore.Http.Features que
los servidores usan para identificar las características que admiten. Las siguientes interfaces de características
controlan las solicitudes y devuelven respuestas:
IHttpRequestFeature Define la estructura de una solicitud HTTP; esto es, el protocolo, la ruta de acceso, la cadena
de consulta, los encabezados y el cuerpo.
IHttpResponseFeature Define la estructura de una respuesta HTTP; esto es, el código de estado, los encabezados y
el cuerpo de la respuesta.
IHttpAuthenticationFeature Define la capacidad para identificar usuarios según un ClaimsPrincipal y para
especificar un controlador de autenticación.
IHttpUpgradeFeature Define la compatibilidad con actualizaciones HTTP, que permiten al cliente especificar otros
protocolos que le gustaría usar en caso de que el servidor cambie de protocolo.
IHttpBufferingFeature Define métodos para deshabilitar el almacenamiento en búfer de solicitudes o respuestas.
IHttpConnectionFeature Define las propiedades de las direcciones y los puertos tanto locales como remotos.
IHttpRequestLifetimeFeature Define la capacidad para anular conexiones o para detectar si una solicitud ha
finalizado antes de tiempo (por ejemplo, debido a una desconexión del cliente).
IHttpSendFileFeature Define un método para enviar archivos de forma asincrónica.
IHttpWebSocketFeature Define una API para admitir sockets web.
IHttpRequestIdentifierFeature Agrega una propiedad que se puede implementar para distinguir solicitudes de
forma única.
ISessionFeature Define abstracciones ISessionFactory e ISession para admitir sesiones de usuario.
ITlsConnectionFeature Define una API para recuperar certificados de cliente.
ITlsTokenBindingFeature Define métodos para trabajar con parámetros de enlace de tokens de TLS.

NOTE
ISessionFeature no es una característica de servidor, pero se implementa por medio de SessionMiddleware . Vea
Introduction to session and application state in ASP.NET Core (Introducción al estado de sesión y la aplicación de ASP.NET
Core).
Colecciones de características
La propiedad Features de HttpContext proporciona una interfaz para obtener y establecer las características
HTTP disponibles para la solicitud actual. Puesto que la colección de características es mutable incluso en el
contexto de una solicitud, se puede usar middleware para modificarla y para agregar compatibilidad con más
características.

Middleware y características de solicitud


Los servidores se encargan de crear la colección de características; el middleware, por su parte, puede agregarse
a esta colección y usar las características de dicha colección. Por ejemplo, StaticFileMiddleware tiene acceso a la
característica IHttpSendFileFeature . Si la característica existe, se usa para enviar el archivo estático solicitado
desde la ruta de acceso física correspondiente. Si no, se usará otro método más lento para enviarlo. Si está
disponible, IHttpSendFileFeature permite al sistema operativo abrir el archivo y realizar una copia directa en
modo kernel en la tarjeta de red.
Además, se puede agregar middleware a la colección de características establecida por el servidor. De hecho, las
características existentes pueden incluso reemplazarse por el middleware, lo que permite que este aumente la
funcionalidad del servidor. Las características agregadas a la colección están disponibles de inmediato para otros
middleware, o bien para la propia aplicación subyacente más adelante en la canalización de solicitudes.
Al combinar implementaciones de servidor personalizadas y mejoras de middleware específicas, se puede
construir el conjunto preciso de características que una aplicación necesita. Gracias a esto, se pueden agregar las
características que faltan sin necesidad de cambiar nada en el servidor y, asimismo, se garantiza que solo quede
expuesta la cantidad mínima de características, lo que reduce el área de superficie de ataques y dispara el
rendimiento.

Resumen
Las interfaces de características definen las características HTTP concretas que una solicitud determinada puede
admitir. Los servidores definen colecciones de características y el conjunto inicial de características admitidas por
esos servidores, pero el middleware puede servir para mejorar estas características.

Recursos adicionales
Servidores
Middleware
Apertura de la interfaz web para .NET (OWIN )
Primitivas en ASP.NET Core
21/06/2018 • 2 minutes to read • Edit Online

Las primitivas de ASP.NET Core son los bloques de creación de bajo nivel compartidos por extensiones de la
plataforma. Puede usar estos bloques de creación en su propio código.
Detectar cambios con tokens de cambio
Detección de cambios con tokens de cambio en
ASP.NET Core
25/05/2018 • 16 minutes to read • Edit Online

Por Luke Latham


Un token de cambio es un bloque de creación de bajo nivel y uso general que se usa para realizar el seguimiento
de los cambios.
Vea o descargue el código de ejemplo (cómo descargarlo)

Interfaz IChangeToken
IChangeToken propaga notificaciones que indican que se ha producido un cambio. IChangeToken reside en el
espacio de nombres Microsoft.Extensions.Primitives. Para las aplicaciones que no usan el metapaquete
Microsoft.AspNetCore.All, haga referencia al paquete NuGet Microsoft.Extensions.Primitives en el archivo de
proyecto.
IChangeToken tiene dos propiedades:
ActiveChangedCallbacks indica si el token genera devoluciones de llamada de forma proactiva. Si
ActiveChangedCallbacks se establece en false , nunca se llama a una devolución de llamada y la aplicación
debe sondear HasChanged en busca de cambios. También es posible que un token nunca se cancele si no se
producen cambios o si se elimina o deshabilita el agente de escucha de cambios subyacente.
HasChanged obtiene un valor que indica si se ha producido un cambio.
La interfaz tiene un método, RegisterChangeCallback(Acción<Objeto>, Objeto), que registra una devolución de
llamada que se invoca cuando el token ha cambiado. HasChanged se debe establecer antes de que se invoque la
devolución de llamada.

Clase ChangeToken
ChangeToken es una clase estática que se usa para propagar notificaciones que indican que se ha producido un
cambio. ChangeToken reside en el espacio de nombres Microsoft.Extensions.Primitives. Para las aplicaciones que
no usan el metapaquete Microsoft.AspNetCore.All, haga referencia al paquete NuGet
Microsoft.Extensions.Primitives en el archivo de proyecto.
El método OnChange(Función<IChangeToken>, Acción) de ChangeToken registra una Action que se llama cada
vez que cambia el token:
Func<IChangeToken> genera el token.
Se llama a Action cuando cambia el token.
ChangeToken tiene una sobrecarga OnChange<TState>(Función<IChangeToken>, Acción<TState>, TState) que
toma un parámetro TState adicional que se pasa a la Action de consumidor de token.
OnChange devuelve una interfaz IDisposable. Al llamar a Dispose se detiene la escucha del token de futuras
modificaciones y se liberan sus recursos.

Ejemplos de uso de tokens de cambio en ASP.NET Core


Los tokens de cambio se usan en áreas principales de ASP.NET Core para la supervisión de cambios en los
objetos:
Para supervisar los cambios en los archivos, el método Watch de IFileProvider crea un IChangeToken para los
archivos especificados o la carpeta que se va a supervisar.
Se pueden agregar tokens IChangeToken a las entradas de caché para desencadenar expulsiones de caché al
producirse un cambio.
Para los cambios de TOptions , la implementación predeterminada OptionsMonitor de IOptionsMonitor tiene
una sobrecarga que acepta una o varias instancias de IOptionsChangeTokenSource. Cada instancia devuelve
un IChangeToken para registrar una devolución de llamada de notificación de cambio a fin de realizar el
seguimiento de los cambios en las opciones.

Supervisión de los cambios de configuración


De forma predeterminada, las plantillas de ASP.NET Core usan archivos de configuración de JSON
(appsettings.json, appsettings.Development.json y appsettings.Production.json) para cargar parámetros de
configuración de la aplicación.
Estos archivos se configuran mediante el método de extensión AddJsonFile(IConfigurationBuilder, String,
Boolean, Boolean) de ConfigurationBuilder, que acepta un parámetro reloadOnChange (ASP.NET Core 1.1 y
versiones posteriores). reloadOnChange indica si la configuración se debe recargar en los cambios de archivo. Vea
esta configuración en el método de conveniencia CreateDefaultBuilder de WebHost:

config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)


.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);

La configuración basada en archivo se representa por medio de FileConfigurationSource.


FileConfigurationSource usa IFileProvider para supervisar los archivos.

PhysicalFileProvider proporciona de forma predeterminada IFileMonitor , que usa FileSystemWatcher para


supervisar los cambios del archivo de configuración.
En la aplicación de ejemplo se muestran dos implementaciones para supervisar los cambios de configuración. Si
cambia el archivo appsettings.json o la versión del entorno del archivo, cada implementación ejecuta código
personalizado. La aplicación de ejemplo escribe un mensaje en la consola.
El FileSystemWatcher de un archivo de configuración puede desencadenar varias devoluciones de llamada de
token para un único cambio del archivo de configuración. La implementación del ejemplo protege contra este
problema mediante la comprobación del hash de archivo en los archivos de configuración. La comprobación del
hash de archivo garantiza que al menos uno de los archivos de configuración ha cambiado antes de ejecutar el
código personalizado. En el ejemplo se usa el hash de archivo SHA1 (Utilities/Utilities.cs):
public static byte[] ComputeHash(string filePath)
{
var runCount = 1;

while(runCount < 4)
{
try
{
if (File.Exists(filePath))
{
using (var fs = File.OpenRead(filePath))
{
return System.Security.Cryptography.SHA1.Create().ComputeHash(fs);
}
}
else
{
throw new FileNotFoundException();
}
}
catch (IOException ex)
{
if (runCount == 3 || ex.HResult != -2147024864)
{
throw;
}
else
{
Thread.Sleep(TimeSpan.FromSeconds(Math.Pow(2, runCount)));
runCount++;
}
}
}

return new byte[20];


}

Se implementa un reintento con una interrupción exponencial. El reintento aparece porque se puede producir un
bloqueo de archivos que impida temporalmente calcular un hash nuevo en uno de los archivos.
Token de cambio de inicio simple
Registre una devolución de llamada de Action de consumidor de token para las notificaciones de cambio en el
token de recarga de configuración (Startup.cs):

ChangeToken.OnChange(
() => config.GetReloadToken(),
(state) => InvokeChanged(state),
env);

config.GetReloadToken() proporciona el token. La devolución de llamada es el método InvokeChanged :


private void InvokeChanged(IHostingEnvironment env)
{
byte[] appsettingsHash = ComputeHash("appSettings.json");
byte[] appsettingsEnvHash =
ComputeHash($"appSettings.{env.EnvironmentName}.json");

if (!_appsettingsHash.SequenceEqual(appsettingsHash) ||
!_appsettingsEnvHash.SequenceEqual(appsettingsEnvHash))
{
_appsettingsHash = appsettingsHash;
_appsettingsEnvHash = appsettingsEnvHash;

WriteConsole("Configuration changed (Simple Startup Change Token)");


}
}

El state de la devolución de llamada se usa para pasar el IHostingEnvironment . Esto es útil para determinar el
archivo JSON de configuración appsettings correcto que se va a supervisar, appsettings.<Entorno>.json. Se usa el
hash de archivo para evitar que se ejecute varias veces la instrucción WriteConsole debido a varias devoluciones
de llamada de token cuando el archivo de configuración solo ha cambiado una vez.
Este sistema se ejecuta siempre que la aplicación esté en ejecución y el usuario no lo puede deshabilitar.
Supervisión de los cambios de configuración como servicio
En el ejemplo se implementa lo siguiente:
La supervisión del token de inicio básico.
La supervisión como servicio.
Un mecanismo para habilitar y deshabilitar la supervisión.
En el ejemplo se establece una interfaz IConfigurationMonitor (Extensions/ConfigurationMonitor.cs):

public interface IConfigurationMonitor


{
bool MonitoringEnabled { get; set; }
string CurrentState { get; set; }
}

El constructor de la clase implementada, ConfigurationMonitor , registra una devolución de llamada para las
notificaciones de cambio:

public ConfigurationMonitor(IConfiguration config, IHostingEnvironment env)


{
_env = env;

ChangeToken.OnChange<IConfigurationMonitor>(
() => config.GetReloadToken(),
InvokeChanged,
this);
}

public bool MonitoringEnabled { get; set; } = false;


public string CurrentState { get; set; } = "Not monitoring";

config.GetReloadToken() proporciona el token. InvokeChanged es el método de devolución de llamada. El


elemento state de esta instancia es una referencia a la instancia de IConfigurationMonitor que se usa para tener
acceso al estado de supervisión. Se usan dos propiedades:
MonitoringEnabled indica si la devolución de llamada debe ejecutar su código personalizado.
CurrentState describe el estado de supervisión actual para su uso en la interfaz de usuario.

El método InvokeChanged es similar al enfoque anterior, excepto en que:


No ejecuta su código, a menos que MonitoringEnabled sea true .
Anota el state actual en su salida de WriteConsole .

private void InvokeChanged(IConfigurationMonitor state)


{
if (MonitoringEnabled)
{
byte[] appsettingsHash = ComputeHash("appSettings.json");
byte[] appsettingsEnvHash =
ComputeHash($"appSettings.{_env.EnvironmentName}.json");

if (!_appsettingsHash.SequenceEqual(appsettingsHash) ||
!_appsettingsEnvHash.SequenceEqual(appsettingsEnvHash))
{
string message = $"State updated at {DateTime.Now}";

_appsettingsHash = appsettingsHash;
_appsettingsEnvHash = appsettingsEnvHash;

WriteConsole($"Configuration changed (ConfigurationMonitor Class) {message}, state:


{state.CurrentState}");
}
}
}

Una instancia de ConfigurationMonitor se registra como servicio en ConfigureServices de Startup.cs:

services.AddSingleton<IConfigurationMonitor, ConfigurationMonitor>();

En la página Index se ofrece al usuario el control sobre la supervisión de la configuración. La instancia de


IConfigurationMonitor se inserta en IndexModel :

public IndexModel(
IConfiguration config,
IConfigurationMonitor monitor,
FileService fileService)
{
_config = config;
_monitor = monitor;
_fileService = fileService;
}

Un botón habilita y deshabilita la supervisión:

<button class="btn btn-danger" asp-page-handler="StopMonitoring">Stop Monitoring</button>


public IActionResult OnPostStartMonitoring()
{
_monitor.MonitoringEnabled = true;
_monitor.CurrentState = "Monitoring!";

return RedirectToPage();
}

public IActionResult OnPostStopMonitoring()


{
_monitor.MonitoringEnabled = false;
_monitor.CurrentState = "Not monitoring";

return RedirectToPage();
}

Cuando se desencadena OnPostStartMonitoring , se habilita la supervisión y se borra el estado actual. Cuando se


desencadena OnPostStopMonitoring , se deshabilita la supervisión y se establece el estado para reflejar que no se
está realizando la supervisión.

Supervisión de los cambios de archivos en caché


El contenido de los archivos se puede almacenar en caché en memoria mediante IMemoryCache. El
almacenamiento en caché en memoria se describe en el tema Cache in-memory (Almacenamiento en caché en
memoria). Sin realizar pasos adicionales, como la implementación que se describe a continuación, si los datos de
origen cambian, se devuelven datos obsoletos (no actualizados) de la caché.
Si no se tiene en cuenta el estado de un archivo de origen en caché cuando se renueva un período de vencimiento
variable, se pueden crear datos en caché obsoletos. En cada solicitud de los datos se renueva el período de
vencimiento variable, pero el archivo nunca se vuelve a cargar en la caché. Las características de la aplicación que
usen el contenido en caché del archivo están sujetas a la posible recepción de contenido obsoleto.
El uso de tokens de cambio en un escenario de almacenamiento en caché de archivos evita contenido de archivo
obsoleto en la caché. En la aplicación de ejemplo se muestra una implementación del enfoque.
En el ejemplo se usa GetFileContent para:
Devolver el contenido del archivo.
Implementar un algoritmo de reintento con interrupción exponencial para casos en los que un bloqueo de
archivo impide temporalmente que se lea un archivo.
Utilities/Utilities.cs:
public async static Task<string> GetFileContent(string filePath)
{
var runCount = 1;

while(runCount < 4)
{
try
{
if (File.Exists(filePath))
{
using (var fileStreamReader = File.OpenText(filePath))
{
return await fileStreamReader.ReadToEndAsync();
}
}
else
{
throw new FileNotFoundException();
}
}
catch (IOException ex)
{
if (runCount == 3 || ex.HResult != -2147024864)
{
throw;
}
else
{
await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, runCount)));
runCount++;
}
}
}

return null;
}

Se crea un FileService para administrar las búsquedas de archivos en caché. La llamada al método
GetFileContent del servicio intenta obtener el contenido de archivo de la caché en memoria y devolverlo al autor
de la llamada (Services/FileService.cs).
Si el contenido en caché no se encuentra mediante la clave de caché, se realizan las acciones siguientes:
1. El contenido del archivo se obtiene mediante GetFileContent .
2. Se obtiene un token de cambio del proveedor de archivos con IFileProviders.Watch. La devolución de llamada
del token se desencadena cuando se modifica el archivo.
3. El contenido del archivo se almacena en caché con un período de vencimiento variable. El token de cambio se
adjunta con MemoryCacheEntryExtensions.AddExpirationToken para expulsar la entrada de caché si el archivo
cambia mientras está almacenado en caché.
public class FileService
{
private readonly IMemoryCache _cache;
private readonly IFileProvider _fileProvider;
private List<string> _tokens = new List<string>();

public FileService(IMemoryCache cache, IHostingEnvironment env)


{
_cache = cache;
_fileProvider = env.ContentRootFileProvider;
}

public async Task<string> GetFileContents(string fileName)


{
// For the purposes of this example, files are stored
// in the content root of the app. To obtain the physical
// path to a file at the content root, use the
// ContentRootFileProvider on IHostingEnvironment.
var filePath = _fileProvider.GetFileInfo(fileName).PhysicalPath;
string fileContent;

// Try to obtain the file contents from the cache.


if (_cache.TryGetValue(filePath, out fileContent))
{
return fileContent;
}

// The cache doesn't have the entry, so obtain the file


// contents from the file itself.
fileContent = await GetFileContent(filePath);

if (fileContent != null)
{
// Obtain a change token from the file provider whose
// callback is triggered when the file is modified.
var changeToken = _fileProvider.Watch(fileName);

// Configure the cache entry options for a five minute


// sliding expiration and use the change token to
// expire the file in the cache if the file is
// modified.
var cacheEntryOptions = new MemoryCacheEntryOptions()
.SetSlidingExpiration(TimeSpan.FromMinutes(5))
.AddExpirationToken(changeToken);

// Put the file contents into the cache.


_cache.Set(filePath, fileContent, cacheEntryOptions);

return fileContent;
}

return string.Empty;
}
}

El FileService se registra en el contenedor de servicios junto con el servicio de almacenamiento en caché


(Startup.cs):

services.AddMemoryCache();
services.AddSingleton<FileService>();

El modelo de página carga el contenido del archivo mediante el servicio (Pages/Index.cshtml.cs):


var fileContent = await _fileService.GetFileContents("poem.txt");

Clase CompositeChangeToken
Para representar una o varias instancias de IChangeToken en un solo objeto, use la clase CompositeChangeToken.

var firstCancellationTokenSource = new CancellationTokenSource();


var secondCancellationTokenSource = new CancellationTokenSource();

var firstCancellationToken = firstCancellationTokenSource.Token;


var secondCancellationToken = secondCancellationTokenSource.Token;

var firstCancellationChangeToken = new CancellationChangeToken(firstCancellationToken);


var secondCancellationChangeToken = new CancellationChangeToken(secondCancellationToken);

var compositeChangeToken =
new CompositeChangeToken(
new List<IChangeToken>
{
firstCancellationChangeToken,
secondCancellationChangeToken
});

En el token compuesto, HasChanged notifica true si algún token representado HasChanged es true . En el token
compuesto, ActiveChangeCallbacks notifica true si algún token representado ActiveChangeCallbacks es true . Si
se producen varios eventos de cambio simultáneos, la devolución de llamada de cambio compuesto se invoca
exactamente una vez.

Vea también
Almacenamiento en caché en memoria
Trabajar con una memoria caché distribuida
Almacenamiento en caché de respuestas
Middleware de almacenamiento en caché de respuestas
Aplicación auxiliar de etiquetas de caché
Aplicación auxiliar de etiquetas de caché distribuida
Interfaz web abierta para .NET (OWIN) con ASP.NET
Core
25/06/2018 • 9 minutes to read • Edit Online

Por Steve Smith y Rick Anderson


ASP.NET Core es compatible con la interfaz web abierta para .NET (OWIN ). OWIN permite que las aplicaciones
web se desacoplen de los servidores web. Define una manera estándar para usar software intermedio en una
canalización a fin de controlar las solicitudes y las respuestas asociadas. El software intermedio y las aplicaciones
de ASP.NET Core pueden interoperar con aplicaciones, servidores y software intermedio basados en OWIN.
OWIN proporciona una capa de desacoplamiento que permite que dos marcos de trabajo con modelos de
objetos dispares se usen juntos. El paquete Microsoft.AspNetCore.Owin proporciona dos implementaciones del
adaptador:
De ASP.NET Core a OWIN
De OWIN a ASP.NET Core
Esto permite que ASP.NET Core se hospede sobre un servidor/host compatible con OWIN, o bien que otros
componentes compatibles con OWIN se ejecuten sobre ASP.NET Core.
Nota: El uso de estos adaptadores conlleva un costo de rendimiento. Las aplicaciones que solo usan
componentes de ASP.NET Core no deben usar el paquete o adaptadores de OWIN.
Vea o descargue el código de ejemplo (cómo descargarlo)

Ejecución de software intermedio de OWIN en la canalización de


ASP.NET
La compatibilidad con OWIN de ASP.NET Core se implementa como parte del paquete
Microsoft.AspNetCore.Owin . Puede importar compatibilidad con OWIN en el proyecto mediante la instalación de
este paquete.
El software intermedio de OWIN cumple la especificación de OWIN, que requiere una interfaz
Func<IDictionary<string, object>, Task> y el establecimiento de determinadas claves (como owin.ResponseBody ).
En el siguiente software intermedio simple de OWIN se muestra "Hello World":

public Task OwinHello(IDictionary<string, object> environment)


{
string responseText = "Hello World via OWIN";
byte[] responseBytes = Encoding.UTF8.GetBytes(responseText);

// OWIN Environment Keys: http://owin.org/spec/spec/owin-1.0.0.html


var responseStream = (Stream)environment["owin.ResponseBody"];
var responseHeaders = (IDictionary<string, string[]>)environment["owin.ResponseHeaders"];

responseHeaders["Content-Length"] = new string[] {


responseBytes.Length.ToString(CultureInfo.InvariantCulture) };
responseHeaders["Content-Type"] = new string[] { "text/plain" };

return responseStream.WriteAsync(responseBytes, 0, responseBytes.Length);


}
La firma de ejemplo devuelve un valor Task y acepta un valor IDictionary<string, object> , según los requisitos
de OWIN.
En el código siguiente se muestra cómo agregar el software intermedio OwinHello (mostrado arriba) a la
canalización ASP.NET con el método de extensión UseOwin .

public void Configure(IApplicationBuilder app)


{
app.UseOwin(pipeline =>
{
pipeline(next => OwinHello);
});
}

Puede configurar la realización de otras acciones en la canalización de OWIN.

NOTE
Los encabezados de respuesta solo deben modificarse antes de la primera escritura en la secuencia de respuesta.

NOTE
No se recomienda la realización de varias llamadas a UseOwin por motivos de rendimiento. Los componentes de OWIN
funcionarán mejor si se agrupan.

app.UseOwin(pipeline =>
{
pipeline(async (next) =>
{
// do something before
await OwinHello(new OwinEnvironment(HttpContext));
// do something after
});
});

Uso de hospedaje de ASP.NET en un servidor basado en OWIN


Los servidores basados en OWIN pueden hospedar aplicaciones de ASP.NET. Un servidor de este tipo es Nowin,
un servidor web de OWIN de .NET. En el ejemplo de este artículo, se ha incluido un proyecto que hace referencia
a Nowin y lo usa para crear una interfaz IServer capaz de autohospedar ASP.NET Core.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;

namespace NowinSample
{
public class Program
{
public static void Main(string[] args)
{
var host = new WebHostBuilder()
.UseNowin()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<Startup>()
.Build();

host.Run();
}
}
}

IServer es una interfaz que requiere una propiedad Features y un método Start .
Start se encarga de configurar e iniciar el servidor, lo que en este caso se lleva a cabo mediante una serie de
llamadas API fluidas que establecen direcciones analizadas desde IServerAddressesFeature. Tenga en cuenta que
la configuración fluida de la variable _builder especifica que las solicitudes se controlarán mediante el valor
appFunc definido anteriormente en el método. Se llama a este valor Func en cada solicitud para procesar las
solicitudes entrantes.
Agregaremos también una extensión IWebHostBuilder para que sea fácil de agregar y configurar el servidor
Nowin.
using System;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.Extensions.DependencyInjection;
using Nowin;
using NowinSample;

namespace Microsoft.AspNetCore.Hosting
{
public static class NowinWebHostBuilderExtensions
{
public static IWebHostBuilder UseNowin(this IWebHostBuilder builder)
{
return builder.ConfigureServices(services =>
{
services.AddSingleton<IServer, NowinServer>();
});
}

public static IWebHostBuilder UseNowin(this IWebHostBuilder builder, Action<ServerBuilder> configure)


{
builder.ConfigureServices(services =>
{
services.Configure(configure);
});
return builder.UseNowin();
}
}
}

Después de hacer estas implementaciones, invoque la extensión en Program.cs para ejecutar una aplicación
ASP.NET a través de este servidor personalizado:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;

namespace NowinSample
{
public class Program
{
public static void Main(string[] args)
{
var host = new WebHostBuilder()
.UseNowin()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<Startup>()
.Build();

host.Run();
}
}
}

Obtenga más información sobre los servidores de ASP.NET.

Ejecución de ASP.NET Core en un servidor basado en OWIN y uso de


su compatibilidad con WebSockets
Otro ejemplo de la manera en que ASP.NET Core puede aprovechar las características de los servidores basados
en OWIN es el acceso a funciones como WebSockets. El servidor web de OWIN de .NET usado en el ejemplo
anterior es compatible con WebSockets integrado, cuyas ventajas puede aprovechar la aplicación ASP.NET Core.
En el ejemplo siguiente se muestra una aplicación web simple que admite WebSockets y devuelve todo lo que se
envía al servidor a través de WebSockets.

public class Startup


{
public void Configure(IApplicationBuilder app)
{
app.Use(async (context, next) =>
{
if (context.WebSockets.IsWebSocketRequest)
{
WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync();
await EchoWebSocket(webSocket);
}
else
{
await next();
}
});

app.Run(context =>
{
return context.Response.WriteAsync("Hello World");
});
}

private async Task EchoWebSocket(WebSocket webSocket)


{
byte[] buffer = new byte[1024];
WebSocketReceiveResult received = await webSocket.ReceiveAsync(
new ArraySegment<byte>(buffer), CancellationToken.None);

while (!webSocket.CloseStatus.HasValue)
{
// Echo anything we receive
await webSocket.SendAsync(new ArraySegment<byte>(buffer, 0, received.Count),
received.MessageType, received.EndOfMessage, CancellationToken.None);

received = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer),


CancellationToken.None);
}

await webSocket.CloseAsync(webSocket.CloseStatus.Value,
webSocket.CloseStatusDescription, CancellationToken.None);
}
}

Este ejemplo se ha configurado con el mismo NowinServer que el anterior; la única diferencia es la manera en
que la aplicación está configurada en su método Configure . La aplicación se muestra mediante una prueba que
usa un cliente WebSocket simple:
Entorno de OWIN
Puede construir un entorno de OWIN por medio de HttpContext .

var environment = new OwinEnvironment(HttpContext);


var features = new OwinFeatureCollection(environment);

Claves de OWIN
OWIN depende de un objeto IDictionary<string,object> para comunicar información mediante un intercambio
de solicitud/respuesta HTTP. ASP.NET Core implementa las claves que se enumeran a continuación. Vea las
extensiones de la especificación principal y las directrices principales y claves comunes de OWIN.
Datos de solicitud (OWIN v1.0.0)
KEY VALOR (TIPO) DESCRIPTION

owin.RequestScheme String

owin.RequestMethod String

owin.RequestPathBase String

owin.RequestPath String

owin.RequestQueryString String

owin.RequestProtocol String

owin.RequestHeaders IDictionary<string,string[]>
KEY VALOR (TIPO) DESCRIPTION

owin.RequestBody Stream

Datos de solicitud (OWIN v1.1.0)


KEY VALOR (TIPO) DESCRIPTION

owin.RequestId String Optional

Datos de respuesta (OWIN v1.0.0)


KEY VALOR (TIPO) DESCRIPTION

owin.ResponseStatusCode int Optional

owin.ResponseReasonPhrase String Optional

owin.ResponseHeaders IDictionary<string,string[]>

owin.ResponseBody Stream

Otros datos (OWIN v1.0.0)


KEY VALOR (TIPO) DESCRIPTION

owin.CallCancelled CancellationToken

owin.Version String

Claves comunes
KEY VALOR (TIPO) DESCRIPTION

ssl.ClientCertificate X509Certificate

ssl.LoadClientCertAsync Func<Task>

server.RemoteIpAddress String

server.RemotePort String

server.LocalIpAddress String

server.LocalPort String

server.IsLocal bool

server.OnSendingHeaders Action<Action<object>,object>

SendFiles v0.3.0
KEY VALOR (TIPO) DESCRIPTION

sendfile.SendAsync Vea Delegate signature (Signatura de Por solicitud


delegado)

Opaque v0.3.0
KEY VALOR (TIPO) DESCRIPTION

opaque.Version String

opaque.Upgrade OpaqueUpgrade Vea Delegate signature (Signatura de


delegado)

opaque.Stream Stream

opaque.CallCancelled CancellationToken

WebSocket v0.3.0
KEY VALOR (TIPO) DESCRIPTION

websocket.Version String

websocket.Accept WebSocketAccept Vea Delegate signature (Signatura de


delegado)

websocket.AcceptAlt Sin especificaciones

websocket.SubProtocol String Vea la sección 4.2.2 de RFC6455, paso


5.5

websocket.SendAsync WebSocketSendAsync Vea Delegate signature (Signatura de


delegado)

websocket.ReceiveAsync WebSocketReceiveAsync Vea Delegate signature (Signatura de


delegado)

websocket.CloseAsync WebSocketCloseAsync Vea Delegate signature (Signatura de


delegado)

websocket.CallCancelled CancellationToken

websocket.ClientCloseStatus int Optional

websocket.ClientCloseDescription String Optional

Recursos adicionales
Middleware
Servidores
Compatibilidad con WebSockets en ASP.NET Core
25/06/2018 • 9 minutes to read • Edit Online

Por Tom Dykstra y Andrew Stanton-Nurse


En este artículo se ofrece una introducción a WebSockets en ASP.NET Core. WebSocket (RFC 6455) es un
protocolo que habilita canales de comunicación bidireccional persistentes a través de conexiones TCP. Se usa en
aplicaciones que sacan partido de comunicaciones rápidas y en tiempo real, como las aplicaciones de chat, panel
y juegos.
Vea o descargue el código de ejemplo (cómo descargarlo). Para más información, vea la sección Pasos siguientes.

Requisitos previos
ASP.NET Core 1.1 o posterior
Cualquier sistema operativo que admita ASP.NET Core:
Windows 7/Windows Server 2008 o posterior
Linux
macOS
Si la aplicación se ejecuta en Windows con IIS:
Windows 8/Windows Server 2012 o versiones posteriores
IIS 8/Express IIS 8
WebSockets debe estar habilitado en IIS (vea la sección Compatibilidad con IIS/IIS Express).
Si la aplicación se ejecuta en HTTP.sys:
Windows 8/Windows Server 2012 o versiones posteriores
Para saber qué exploradores son compatibles, vea https://caniuse.com/#feat=websockets.

Cuándo usar WebSockets


Use WebSockets para trabajar directamente con una conexión de socket. Por ejemplo, úselo para lograr el mejor
rendimiento posible en un juego en tiempo real.
ASP.NET SignalR proporciona un modelo de aplicación más completo para funciones en tiempo real, pero solo
se ejecuta en ASP.NET 4.x, no en ASP.NET Core. Hay una versión de SignalR para ASP.NET Core programada
para su lanzamiento con ASP.NET Core 2.1. Vea ASP.NET Core 2.1 high-level planning (Planeación de alto nivel
de ASP.NET Core 2.1).
Hasta el lanzamiento de SignalR Core, puede usar WebSockets, pero mientras tanto el desarrollador deberá
suministrar y admitir las características que SignalR proporciona. Por ejemplo:
Compatibilidad con una gama más amplia de versiones del explorador, ya que usa la reserva automática para
los métodos de transporte alternativos.
Reconexión automática cuando se produce un fallo de conexión.
Compatibilidad con clientes que llaman a métodos en el servidor o viceversa.
Compatibilidad con el escalado a varios servidores.

Cómo se usa
Instale el paquete Microsoft.AspNetCore.WebSockets.
Configure el middleware.
Acepte las solicitudes WebSocket.
Envíe y reciba mensajes.
Configurar el middleware
Agregue el middleware de WebSockets al método Configure de la clase Startup :

app.UseWebSockets();

Se pueden configurar estas opciones:


KeepAliveInterval : la frecuencia con que se envían marcos "ping" al cliente, para asegurarse de que los
servidores proxy mantienen abierta la conexión.
ReceiveBufferSize : el tamaño del búfer usado para recibir datos. Puede que los usuarios avanzados tengan
que cambiar estas opciones para ajustar el rendimiento según el tamaño de los datos.

var webSocketOptions = new WebSocketOptions()


{
KeepAliveInterval = TimeSpan.FromSeconds(120),
ReceiveBufferSize = 4 * 1024
};
app.UseWebSockets(webSocketOptions);

Aceptar solicitudes WebSocket


En algún momento posterior en el ciclo de solicitudes (más adelante en el método Configure o en una acción de
MVC, por ejemplo) debe comprobar si se trata de una solicitud WebSocket y aceptarla.
El siguiente ejemplo se corresponde con un momento más adelante en el método Configure :

app.Use(async (context, next) =>


{
if (context.Request.Path == "/ws")
{
if (context.WebSockets.IsWebSocketRequest)
{
WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync();
await Echo(context, webSocket);
}
else
{
context.Response.StatusCode = 400;
}
}
else
{
await next();
}

});

Una solicitud WebSocket puede proceder de cualquier dirección URL, pero este código de ejemplo solo acepta
solicitudes de /ws .
Enviar y recibir mensajes
El método AcceptWebSocketAsync actualiza la conexión TCP a una conexión WebSocket y proporciona un objeto
WebSocket. Use el objeto WebSocket para enviar y recibir mensajes.
El código antes mostrado que acepta la solicitud WebSocket pasa el objeto WebSocket a un método Echo . El
código recibe un mensaje y devuelve inmediatamente el mismo mensaje. Los mensajes se envían y reciben en un
bucle hasta que el cliente cierra la conexión:

private async Task Echo(HttpContext context, WebSocket webSocket)


{
var buffer = new byte[1024 * 4];
WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer),
CancellationToken.None);
while (!result.CloseStatus.HasValue)
{
await webSocket.SendAsync(new ArraySegment<byte>(buffer, 0, result.Count), result.MessageType,
result.EndOfMessage, CancellationToken.None);

result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);


}
await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription,
CancellationToken.None);
}

Cuando la conexión WebSocket se acepta antes de que el bucle comience, la canalización de middleware finaliza.
Tras cerrar el socket, se desenreda la canalización. Es decir, la solicitud deja de avanzar en la canalización cuando
WebSocket se acepta, pero cuando el bucle termina y el socket se cierra, la solicitud vuelve a recorrer la
canalización.

Compatibilidad con IIS/IIS Express


El protocolo WebSocket se puede usar en Windows Server 2012 o posterior, y en Windows 8 o posterior con IIS
o IIS Express 8 o posterior.
Para habilitar la compatibilidad con el protocolo WebSocket en Windows Server 2012 o posterior:
1. Use el asistente Agregar roles y características del menú Administrar o el vínculo de Administrador del
servidor.
2. Seleccione Instalación basada en características o en roles. Seleccione Siguiente.
3. Seleccione el servidor que corresponda (el servidor local está seleccionado de forma predeterminada).
Seleccione Siguiente.
4. Expanda Servidor web (IIS ) en el árbol Roles, expanda Servidor web y, por último, expanda Desarrollo de
aplicaciones.
5. Seleccione Protocolo WebSocket. Seleccione Siguiente.
6. Si no necesita más características, haga clic en Siguiente.
7. Haga clic en Instalar.
8. Cuando la instalación finalice, haga clic en Cerrar para salir del asistente.
Para habilitar la compatibilidad con el protocolo WebSocket en Windows Server 8 o posterior:
1. Vaya a Panel de control > Programas > Programas y características > Activar o desactivar las
características de Windows (lado izquierdo de la pantalla).
2. Abra los siguientes nodos: Internet Information Services > Servicios World Wide Web >
Características de desarrollo de aplicaciones.
3. Seleccione la característica Protocolo WebSocket. Seleccione Aceptar.
Deshabilitar WebSocket al usar socket.io en node.js
Si usa la compatibilidad de WebSocket en socket.io en Node.js, deshabilite el módulo IIS WebSocket
predeterminado, usando para ello el elemento webSocket de web.config o de applicationHost.config. Si este paso
no se lleva a cabo, el módulo IIS WebSocket intenta controlar la comunicación de WebSocket en lugar de
Node.js y la aplicación.

<system.webServer>
<webSocket enabled="false" />
</system.webServer>

Pasos siguientes
La aplicación de ejemplo que acompaña a este artículo es una aplicación de eco. Tiene una página web que
realiza las conexiones WebSocket y el servidor reenvía de vuelta al cliente todos los mensajes que reciba. Ejecute
la aplicación desde un símbolo del sistema (no está configurada para ejecutarse desde Visual Studio con IIS
Express) y vaya a http://localhost:5000. En la página web se muestra el estado de conexión en la parte superior
izquierda:

Seleccione Connect (Conectar) para enviar una solicitud WebSocket para la URL mostrada. Escriba un mensaje
de prueba y seleccione Send (Enviar). Cuando haya terminado, seleccione Close Socket (Cerrar socket). Los
informes de la sección Communication Log (Registro de comunicación) informan de cada acción de abrir,
enviar y cerrar a medida que se producen.
Metapaquete Microsoft.AspNetCore.App para
ASP.NET Core 2.1
19/06/2018 • 6 minutes to read • Edit Online

Esta característica requiere que ASP.NET Core 2.1 y versiones posteriores tengan como destino .NET Core 2.1 y
versiones posteriores.
El metapaquete Microsoft.AspNetCore.App para ASP.NET Core:
No incluye dependencias de terceros excepto Json.NET, Remotion.Linq e IX-Async. Estas dependencias de
terceros se consideran necesarias para garantizar el funcionamiento de las características de los principales
marcos.
Incluye todos los paquetes admitidos por el equipo de ASP.NET Core, excepto aquellos que contienen
dependencias de terceros (distintos de los mencionados anteriormente).
Incluye todos los paquetes admitidos por el equipo de Entity Framework Core, excepto aquellos que
contienen dependencias de terceros (distintos de los mencionados anteriormente).
Todas las características de ASP.NET Core 2.1 y versiones posteriores, así como de Entity Framework Core 2.1 y
versiones posteriores, están incluidas en el paquete Microsoft.AspNetCore.App . Las plantillas de proyecto
predeterminada destinadas a ASP.NET 2.1 y versiones posteriores usan este paquete. Se recomienda que las
aplicaciones que tengan como destino ASP.NET Core 2.1 y versiones posteriores, así como Entity Framework
Core 2.1 y versiones posteriores, usen el paquete Microsoft.AspNetCore.App .
El número de versión del metapaquete Microsoft.AspNetCore.App representa la versión de ASP.NET Core y la
versión de Entity Framework Core.
Mediante el metapaquete Microsoft.AspNetCore.App se proporcionan restricciones de versión que protegen la
aplicación:
Si se incluye un paquete que tiene una dependencia transitiva (no directa) en un paquete en
Microsoft.AspNetCore.App y los números de versión son distintos, NuGet generará un error.
Los demás paquetes agregados a la aplicación no pueden cambiar la versión de los paquetes que se incluyen
en Microsoft.AspNetCore.App .
La coherencia de versiones garantiza una experiencia fiable. Microsoft.AspNetCore.App se ha diseñado para
evitar las combinaciones de versiones no probadas de bits relacionados que se usan conjuntamente en la
misma aplicación.
Las aplicaciones que usan el metapaquete Microsoft.AspNetCore.App pueden aprovechar automáticamente el
marco de uso compartido de ASP.NET Core. Al usar el metapaquete Microsoft.AspNetCore.App , no se
implementa ningún recurso de los paquetes NuGet de ASP.NET Core a los que se hace referencia con la
aplicación, porque el marco de uso compartido de ASP.NET Core ya contiene esos recursos. Los recursos del
marco de uso compartido se precompilan para mejorar el tiempo de inicio de la aplicación. Para más
información, vea "Marco de uso compartido" en Empaquetado de distribución de .NET Core.
El siguiente archivo de proyecto .csproj hace referencia al metapaquete Microsoft.AspNetCore.App de ASP.NET
Core:
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>

</Project>

El marcado anterior representa una plantilla típica de ASP.NET Core 2.1 y versiones posteriores. No especifica
ningún número de versión para la referencia del paquete Microsoft.AspNetCore.App . Si no se especifica la
versión, el SDK define una versión implícita, es decir, Microsoft.NET.Sdk.Web . Es recomendable confiar en la
versión implícita especificada por el SDK y no establecer de forma explícita el número de versión en la referencia
del paquete. Puede dejar un comentario de GitHub en Discussion for the Microsoft.AspNetCore.App implicit
version (Debate sobre la versión implícita Microsoft.AspNetCore.App).
La versión implícita se establece en major.minor.0 para las aplicaciones portátiles. El mecanismo de puesta al
día del marco de uso compartido ejecutará la aplicación en la versión más reciente compatible entre los marcos
de uso compartidos instalados. Para garantizar que se use la misma versión en el desarrollo, las pruebas y la
producción, asegúrese de que en todos los entornos esté instalada la misma versión del marco de uso
compartido. Para las aplicaciones autocontenidas, el número de versión implícita se establece en el valor
major.minor.patch del marco de uso compartido incluido en el SDK instalado.

El hecho de especificar un número de versión en la referencia de Microsoft.AspNetCore.App no garantiza que se


vaya a elegir la versión del marco de uso compartido. Por ejemplo, suponga que se especifica la versión "2.1.1",
pero está instalada la "2.1.3". En ese caso, la aplicación usará el valor "2.1.3". Aunque no se recomienda, puede
deshabilitar la puesta al día (revisión o secundaria). Para obtener más información sobre la puesta al día del host
de dotnet y cómo configurar su comportamiento, vea Dotnet host roll forward (Puesta al día del host de dotnet).
El metapaquete Microsoft.AspNetCore.App no es un paquete habitual que se actualice desde NuGet. De forma
similar a Microsoft.NETCore.App , Microsoft.AspNetCore.App representa un tiempo de ejecución compartido, con
una semántica especial de control de versiones controlada de forma ajena a NuGet. Para obtener más
información, vea Paquetes, metapaquetes y marcos de trabajo.
Si la aplicación ha usado Microsoft.AspNetCore.All anteriormente, consulte Migración desde
Microsoft.AspNetCore.All a Microsoft.AspNetCore.App.
Metapaquete Microsoft.AspNetCore.All para
ASP.NET Core 2.0
25/06/2018 • 3 minutes to read • Edit Online

NOTE
Se recomienda que las aplicaciones que tengan como destino ASP.NET Core 2.1 y versiones posteriores usen
Microsoft.AspNetCore.App en lugar de este paquete. Consulte Migración desde Microsoft.AspNetCore.All a
Microsoft.AspNetCore.App en este artículo.

Esta característica requiere ASP.NET Core 2.x con .NET Core 2.x como destino.
El metapaquete Microsoft.AspNetCore.All para ASP.NET Core incluye lo siguiente:
Todos los paquetes admitidos por el equipo de ASP.NET Core.
Todos los paquetes admitidos por Entity Framework Core.
Dependencias internas y de terceros usadas por ASP.NET Core y Entity Framework Core.
Todas las características de ASP.NET Core 2.x y Entity Framework Core 2.x están incluidas en el paquete
Microsoft.AspNetCore.All . Las plantillas de proyecto predeterminada destinadas a ASP.NET 2.0 usan este
paquete.
El número de versión del metapaquete Microsoft.AspNetCore.All representa la versión de ASP.NET Core y la
versión de Entity Framework Core.
Las aplicaciones que usan el metapaquete Microsoft.AspNetCore.All pueden aprovechar automáticamente el
almacén en tiempo de ejecución de .NET Core. El almacén en tiempo de ejecución contiene todos los
recursos en tiempo de ejecución necesarios para ejecutar aplicaciones de ASP.NET Core 2.x. Al usar el
metapaquete Microsoft.AspNetCore.All , no se implementa ningún recurso de los paquetes NuGet de
ASP.NET Core a los que se hace referencia con la aplicación, porque el almacén en tiempo de ejecución de
.NET Core ya contiene esos recursos. Los recursos del almacén en tiempo de ejecución se precompilan para
mejorar el tiempo de inicio de la aplicación.
Puede usar el proceso de recorte de paquetes para quitar los paquetes que no se usan. Los paquetes
recortados se excluyen de la salida de la aplicación publicada.
El siguiente archivo .csproj hace referencia al metapaquete Microsoft.AspNetCore.All de ASP.NET Core:

<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" />
</ItemGroup>
</Project>

Migración desde Microsoft.AspNetCore.All a


Microsoft.AspNetCore.App
En Microsoft.AspNetCore.All se incluyen los siguientes paquetes, pero no el paquete
Microsoft.AspNetCore.App .
Microsoft.AspNetCore.ApplicationInsights.HostingStartup
Microsoft.AspNetCore.AzureAppServices.HostingStartup
Microsoft.AspNetCore.AzureAppServicesIntegration
Microsoft.AspNetCore.DataProtection.AzureKeyVault
Microsoft.AspNetCore.DataProtection.AzureStorage
Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv
Microsoft.AspNetCore.SignalR.Redis
Microsoft.Data.Sqlite
Microsoft.Data.Sqlite.Core
Microsoft.EntityFrameworkCore.Sqlite
Microsoft.EntityFrameworkCore.Sqlite.Core
Microsoft.Extensions.Caching.Redis
Microsoft.Extensions.Configuration.AzureKeyVault
Microsoft.Extensions.Logging.AzureAppServices
Microsoft.VisualStudio.Web.BrowserLink

Para pasar de Microsoft.AspNetCore.All a Microsoft.AspNetCore.App , si su aplicación usa las API de los


paquetes anteriores, o bien paquetes incluidos en ellos, agregue las referencias correspondientes a dichos
paquetes en el proyecto.
No se incluye implícitamente ninguna dependencia de los paquetes anteriores que no sea, de otro modo, una
dependencia de Microsoft.AspNetCore.App . Por ejemplo:
StackExchange.Redis como dependencia de Microsoft.Extensions.Caching.Redis
Microsoft.ApplicationInsights como dependencia de
Microsoft.AspNetCore.ApplicationInsights.HostingStartup
2 minutes to read
Información general de ASP.NET Core MVC
10/04/2018 • 19 minutes to read • Edit Online

Por Steve Smith


ASP.NET Core MVC es un completo marco de trabajo para compilar aplicaciones web y API mediante el patrón
de diseño Modelo-Vista-Controlador.

¿Qué es el patrón de MVC?


El patrón de arquitectura del controlador de vista de modelos (MVC ) separa una aplicación en tres grupos de
componentes principales: modelos, vistas y controladores. Este patrón permite lograr la separación de intereses.
Con este patrón, las solicitudes del usuario se enrutan a un controlador que se encarga de trabajar con el
modelo para realizar las acciones del usuario o recuperar los resultados de consultas. El controlador elige la vista
para mostrar al usuario y proporciona cualquier dato de modelo que sea necesario.
En el siguiente diagrama se muestran los tres componentes principales y cuál hace referencia a los demás:

Con esta delineación de responsabilidades es más sencillo escalar la aplicación, porque resulta más fácil
codificar, depurar y probar algo (modelo, vista o controlador) que tenga un solo trabajo (y siga el principio de
responsabilidad única ). Es más difícil actualizar, probar y depurar código que tenga dependencias repartidas
entre dos o más de estas tres áreas. Por ejemplo, la lógica de la interfaz de usuario tiende a cambiar con mayor
frecuencia que la lógica de negocios. Si el código de presentación y la lógica de negocios se combinan en un solo
objeto, un objeto que contenga lógica de negocios deberá modificarse cada vez que cambie la interfaz de
usuario. A menudo esto genera errores y es necesario volver a probar la lógica de negocio después de cada
cambio mínimo en la interfaz de usuario.

NOTE
La vista y el controlador dependen del modelo. Sin embargo, el modelo no depende de la vista ni del controlador. Esta es
una de las ventajas principales de la separación. Esta separación permite compilar y probar el modelo con independencia
de la presentación visual.

Responsabilidades del modelo


El modelo en una aplicación de MVC representa el estado de la aplicación y cualquier lógica de negocios u
operaciones que esta deba realizar. La lógica de negocios debe encapsularse en el modelo, junto con cualquier
lógica de implementación para conservar el estado de la aplicación. Las vistas fuertemente tipadas normalmente
usan tipos ViewModel diseñados para que contengan los datos para mostrar en esa vista. El controlador crea y
rellena estas instancias de ViewModel desde el modelo.

NOTE
Hay muchas maneras de organizar el modelo en una aplicación que usa el patrón de arquitectura de MVC. Obtenga más
información sobre algunos tipos diferentes de tipos de modelo.

Responsabilidades de las vistas


Las vistas se encargan de presentar el contenido a través de la interfaz de usuario. Usan el motor de vistas de
Razor para incrustar código .NET en formato HTML. Debería haber la mínima lógica entre las vistas y cualquier
lógica en ellas debe estar relacionada con la presentación de contenido. Si ve que necesita realizar una gran
cantidad de lógica en los archivos de vistas para mostrar datos de un modelo complejo, considere la opción de
usar un componente de vista, ViewModel, o una plantilla de vista para simplificar la vista.
Responsabilidades del controlador
Los controladores son los componentes que controlan la interacción del usuario, trabajan con el modelo y, en
última instancia, seleccionan una vista para representarla. En una aplicación de MVC, la vista solo muestra
información; el controlador controla y responde a la interacción y los datos que introducen los usuarios. En el
patrón de MVC, el controlador es el punto de entrada inicial que se encarga de seleccionar con qué tipos de
modelo trabajar y qué vistas representar (de ahí su nombre, ya que controla el modo en que la aplicación
responde a una determinada solicitud).

NOTE
Los controladores no deberían complicarse con demasiadas responsabilidades. Para evitar que la lógica del controlador se
vuelva demasiado compleja, use el principio de responsabilidad única para sacar la lógica de negocios fuera el controlador
y llevarla al modelo de dominio.

TIP
Si piensa que el controlador realiza con frecuencia los mismos tipos de acciones, puede seguir el principio Una vez y solo
una (DRY) moviendo estas acciones comunes a filtros.

Qué es ASP.NET Core MVC


El marco de ASP.NET Core MVC es un marco de presentación ligero, de código abierto y con gran capacidad de
prueba, que está optimizado para usarlo con ASP.NET Core.
ASP.NET Core MVC ofrece una manera basada en patrones de crear sitios web dinámicos que permitan una
clara separación de intereses. Proporciona control total sobre el marcado, admite el desarrollo controlado por
pruebas (TDD ) y usa los estándares web más recientes.

Características
ASP.NET Core MVC incluye lo siguiente:
Enrutamiento
Enlace de modelos
Validación de modelos
Inserción de dependencias
Filtros
Áreas
API web
Capacidad de prueba
Motor de vistas de Razor
Vistas fuertemente tipadas
Aplicaciones auxiliares de etiquetas
Componentes de vista
Enrutamiento
ASP.NET Core MVC se basa en el enrutamiento de ASP.NET Core, un eficaz componente de asignación de URL
que permite compilar aplicaciones que tengan direcciones URL comprensibles y que admitan búsquedas. Esto
permite definir patrones de nomenclatura de URL de la aplicación que funcionen bien para la optimización del
motor de búsqueda (SEO ) y para la generación de vínculos, sin tener en cuenta cómo se organizan los archivos
en el servidor web. Puede definir las rutas mediante una sintaxis de plantilla de ruta adecuada que admita las
restricciones de valores de ruta, los valores predeterminados y los valores opcionales.
El enrutamiento basado en la convención permite definir globalmente los formatos de URL que acepta la
aplicación y cómo cada uno de esos formatos se asigna a un método de acción específico en un determinado
controlador. Cuando se recibe una solicitud entrante, el motor de enrutamiento analiza la URL y la hace coincidir
con uno de los formatos de URL definidos. Después, llama al método de acción del controlador asociado.

routes.MapRoute(name: "Default", template: "{controller=Home}/{action=Index}/{id?}");

El enrutamiento de atributos permite especificar información de enrutamiento mediante la asignación de


atributos a los controladores y las acciones, que permiten definir las rutas de la aplicación. Esto significa que las
definiciones de ruta se colocan junto al controlador y la acción con la que están asociados.

[Route("api/[controller]")]
public class ProductsController : Controller
{
[HttpGet("{id}")]
public IActionResult GetProduct(int id)
{
...
}
}

Enlace de modelos
El enlace de modelo de ASP.NET Core MVC convierte los datos de solicitud del cliente (valores de formulario,
datos de ruta, parámetros de cadena de consulta, encabezados HTTP ) en objetos que el controlador puede
controlar. Como resultado, la lógica del controlador no tiene que realizar el trabajo de pensar en los datos de la
solicitud entrante; simplemente tiene los datos como parámetros para los métodos de acción.

public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null) { ... }

Validación de modelos
ASP.NET Core MVC admite la validación decorando el objeto de modelo con los atributos de validación de
anotación de datos. Los atributos de validación se comprueban en el lado cliente antes de que los valores se
registren en el servidor, y también en el servidor antes de llamar a la acción del controlador.
using System.ComponentModel.DataAnnotations;
public class LoginViewModel
{
[Required]
[EmailAddress]
public string Email { get; set; }

[Required]
[DataType(DataType.Password)]
public string Password { get; set; }

[Display(Name = "Remember me?")]


public bool RememberMe { get; set; }
}

Una acción del controlador:

public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)


{
if (ModelState.IsValid)
{
// work with the model
}
// At this point, something failed, redisplay form
return View(model);
}

El marco administra los datos de la solicitud de validación en el cliente y en el servidor. La lógica de validación
especificada en tipos de modelo se agrega a las vistas representadas como anotaciones discretas y se aplica en el
explorador con Validación de jQuery.
Inserción de dependencias
ASP.NET Core tiene compatibilidad integrada con la inserción de dependencias. En ASP.NET Core MVC, los
controladores pueden solicitar los servicios que necesiten a través de sus constructores, lo que les permite
seguir el principio de dependencias explícitas.
La aplicación también puede usar la inserción de dependencias en archivos de vistas, usando la directiva
@inject :

@inject SomeService ServiceName


<!DOCTYPE html>
<html lang="en">
<head>
<title>@ServiceName.GetTitle</title>
</head>
<body>
<h1>@ServiceName.GetTitle</h1>
</body>
</html>

Filtros
Los filtros ayudan a los desarrolladores a encapsular ciertas preocupaciones transversales, como el control de
excepciones o la autorización. Los filtros habilitan la ejecución de lógica de procesamiento previo y posterior
para métodos de acción y pueden configurarse para ejecutarse en ciertos puntos dentro de la canalización de
ejecución para una solicitud determinada. Los filtros pueden aplicarse a controladores o acciones como atributos
(o pueden ejecutarse globalmente). En el marco se incluyen varios filtros (como Authorize ).
[Authorize]
public class AccountController : Controller
{

Áreas
Las áreas ofrecen una manera de dividir una aplicación web ASP.NET Core MVC de gran tamaño en
agrupaciones funcionales más pequeñas. Un área es una estructura de MVC dentro de una aplicación. En un
proyecto de MVC, los componentes lógicos como el modelo, el controlador y la vista se guardan en carpetas
diferentes, y MVC usa las convenciones de nomenclatura para crear la relación entre estos componentes. Para
una aplicación grande, puede ser conveniente dividir la aplicación en distintas áreas de funciones de alto nivel.
Por ejemplo, una aplicación de comercio electrónico con varias unidades de negocio, como la finalización de la
compra, la facturación, la búsqueda, etcétera. Cada una de estas unidades tiene sus propias vistas, controladores
y modelos de componentes lógicos.
API web
Además de ser una plataforma excelente para crear sitios web, ASP.NET Core MVC es muy compatible con la
creación de las API web. Puede crear servicios que lleguen con facilidad a una amplia gama de clientes, incluidos
exploradores y dispositivos móviles.
El marco de trabajo incluye compatibilidad para la negociación de contenido HTTP con la compatibilidad
integrada para dar formato a datos como JSON o XML. Escriba formateadores personalizados para agregar
compatibilidad con sus propios formatos.
Use la generación de vínculos para habilitar la compatibilidad con hipermedios. Habilite fácilmente la
compatibilidad con el uso compartido de recursos entre orígenes (CORS ) para que las API web se pueden
compartir entre varias aplicaciones web.
Capacidad de prueba
Uso del marco de trabajo de inserción de dependencias e interfaces hacer adecuadas a las pruebas unitarias y el
marco de trabajo incluye características (por ejemplo, un proveedor TestHost y InMemory para Entity
Framework) que hacen pruebas de integración rápido y fácil así. Obtenga más información sobre cómo probar
la lógica del controlador.
Motor de vistas de Razor
Las vistas de ASP.NET Core MVC usan el motor de vistas de Razor para representar vistas. Razor es un lenguaje
de marcado de plantillas compacto, expresivo y fluido para definir vistas que usan código incrustado de C#.
Razor se usa para generar dinámicamente contenido web en el servidor. Permite combinar de manera limpia el
código del servidor con el código y el contenido del lado cliente.

<ul>
@for (int i = 0; i < 5; i++) {
<li>List item @i</li>
}
</ul>

Con el motor de vistas de Razor, puede definir diseños, vistas parciales y secciones reemplazables.
Vistas fuertemente tipadas
Las vistas de Razor en MVC pueden estar fuertemente tipadas en función del modelo. Los controladores
pueden pasar un modelo fuertemente tipado a las vistas para que estas admitan IntelliSense y la comprobación
de tipos.
Por ejemplo, en esta vista se representa un modelo de tipo IEnumerable<Product> :
@model IEnumerable<Product>
<ul>
@foreach (Product p in Model)
{
<li>@p.Name</li>
}
</ul>

Aplicaciones auxiliares de etiquetas


Las aplicaciones auxiliares de etiquetas permiten que el código del lado servidor participe en la creación y la
representación de elementos HTML en archivos de Razor. Puede usar aplicaciones auxiliares de etiquetas para
definir etiquetas personalizadas (por ejemplo, <environment> ) o para modificar el comportamiento de etiquetas
existentes (por ejemplo, <label> ). Las aplicaciones auxiliares de etiquetas enlazan con elementos específicos, en
función del nombre del elemento y sus atributos. Proporcionan las ventajas de la representación del lado
servidor, al tiempo que se mantiene una experiencia de edición HTML.
Hay muchas aplicaciones auxiliares de etiquetas integradas para tareas comunes (como la creación de
formularios, vínculos, carga de activos, etc.) y existen muchas más a disposición en repositorios públicos de
GitHub y como paquetes NuGet. Las aplicaciones auxiliares de etiquetas se crean en C# y tienen como destino
elementos HTML en función del nombre de elemento, el nombre de atributo o la etiqueta principal. Por ejemplo,
la aplicación auxiliar de etiquetas integrada LinkTagHelper puede usarse para crear un vínculo a la acción Login
de AccountsController :

<p>
Thank you for confirming your email.
Please <a asp-controller="Account" asp-action="Login">Click here to Log in</a>.
</p>

La aplicación auxiliar de etiquetas EnvironmentTagHelper puede usarse para incluir distintos scripts en las vistas
(por ejemplo, sin formato o reducida) según el entorno en tiempo de ejecución, como desarrollo, ensayo o
producción:

<environment names="Development">
<script src="~/lib/jquery/dist/jquery.js"></script>
</environment>
<environment names="Staging,Production">
<script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-2.1.4.min.js"
asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
asp-fallback-test="window.jQuery">
</script>
</environment>

Las aplicaciones auxiliares de etiquetas ofrecen una experiencia de desarrollo compatible con HTML y un
entorno de IntelliSense enriquecido para crear formato HTML y Razor. La mayoría de las aplicaciones auxiliares
de etiquetas integradas tienen como destino elementos HTML existentes y proporcionan atributos del lado
servidor para el elemento.
Componentes de vista
Los componentes de vista permiten empaquetar la lógica de representación y reutilizarla en toda la aplicación.
Son similares a las vistas parciales, pero con lógica asociada.
Enlace de modelos en ASP.NET Core
25/06/2018 • 15 minutes to read • Edit Online

Por Rachel Appel

Introducción enlace de modelos


El enlace de modelos en ASP.NET Core MVC asigna datos de solicitudes HTTP a parámetros de
método de acción. Los parámetros pueden ser tipos simples, como cadenas, enteros o flotantes, o
pueden ser tipos complejos. Se trata de una excelente característica de MVC porque la asignación
de los datos entrantes a un equivalente es un escenario muy frecuente, independientemente del
tamaño o la complejidad de los datos. MVC soluciona este problema abstrayendo el enlace para
que los desarrolladores no tengan que seguir reescribiendo una versión algo diferente de ese
mismo código en cada aplicación. Escribir su propio texto o código para el convertidor de tipos es
una tarea aburrida y proclive a errores.

Cómo funciona el enlace de modelos


Cuando MVC recibe una solicitud HTTP, la enruta a un método de acción específico de un
controlador. Determina qué método de acción se ejecutará en función del contenido de los datos de
ruta y luego enlaza valores de la solicitud HTTP a los parámetros de ese método de acción.
Pongamos como ejemplo esta URL:
http://contoso.com/movies/edit/2

Puesto que la plantilla de ruta tiene este aspecto, {controller=Home}/{action=Index}/{id?} ,


movies/edit/2 enruta al controlador Movies y su método de acción Edit . También acepta un
parámetro opcional denominado id . El código para el método de acción debe parecerse a esto:

public IActionResult Edit(int? id)

Nota: Las cadenas de la ruta de URL no distinguen mayúsculas de minúsculas.


MVC intentará enlazar los datos de la solicitud a los parámetros de acción por su nombre. MVC
buscará valores para cada parámetro mediante el nombre del parámetro y los nombres de sus
propiedades públicas configurables. En el ejemplo anterior, el único parámetro de acción se
denomina id , el cual MVC enlaza al valor con el mismo nombre en los valores de ruta. Además de
los valores de ruta, MVC enlazará datos de varias partes de la solicitud y lo hará en un orden
establecido. Esta es una lista de los orígenes de datos en el orden en que el enlace de modelos
busca en ellos:
1. Form values : son valores de formulario que van en la solicitud HTTP mediante el método
POST. incluidas las solicitudes POST de jQuery).
(
2. Route values : conjunto de valores de ruta proporcionados por el enrutamiento
3. Query strings : elemento de cadena de consulta del URI.
Nota: Los valores de formulario, los datos de enrutamiento y las cadenas de consulta se almacenan
como pares de nombre-valor.
Como el enlace de modelos pidió una clave con el nombre id y no hay nada denominado id en
los valores de formulario, continuó con los valores de ruta buscando esa clave. En nuestro ejemplo,
es una coincidencia. Se produce el enlace y el valor se convierte en el entero 2. La misma solicitud
mediante Edit(string id) se convertiría en la cadena "2".
Hasta ahora en el ejemplo se usan tipos simples. En MVC, los tipos simples son cualquier tipo o
tipo primitivo de .NET con un convertidor de tipos de cadena. Si el parámetro del método de acción
fuera una clase como el tipo Movie , que contiene tipos simples y complejos como propiedades, el
enlace de modelos de MVC seguiría controlándolo correctamente. Usa reflexión y recursividad
para recorrer las propiedades de tipos complejos buscando coincidencias. El enlace de modelos
busca el patrón parameter_name.property_name para enlazar los valores a las propiedades. Si no
encuentra los valores coincidentes con esta forma, intentará enlazar usando solo el nombre de
propiedad. Para esos tipos, como los tipos Collection , el enlace de modelos busca coincidencias
para parameter_name[index] o solo [index]. En enlace de modelos trata los tipos Dictionary del
mismo modo, preguntando por parameter_name [key ] o simplemente [key ], siempre que las claves
sean tipos simples. Las claves compatibles coinciden con el HTML de nombres de campos y las
aplicaciones auxiliares de etiquetas generadas para el mismo tipo de modelo. Esto permite realizar
un recorrido de ida y vuelta para que los campos del formulario permanezcan rellenados con los
datos del usuario para su comodidad (por ejemplo, cuando los datos enlazados de una creación o
una edición no superaron la validación).
Para que se produzca el enlace, la clase debe tener un constructor público predeterminado y el
miembro que se va a enlazar debe ser una propiedad pública de escritura. Cuando se produce el
enlace de modelos, la instancia de la clase se creará usando solamente el constructor
predeterminado público y, después, ya se podrán establecer las propiedades.
Cuando se enlaza un parámetro, el enlace de modelos deja de buscar valores con ese nombre y
pasa a enlazar el siguiente parámetro. En caso contrario, el comportamiento predeterminado del
enlace de modelos establece los parámetros en sus valores predeterminados según el tipo:
T[] : con la excepción de matrices de tipo byte[] , el enlace establece parámetros de tipo
T[] a Array.Empty<T>() . Las matrices de tipo byte[] se establecen en null .

Tipos de referencia: el enlace crea una instancia de una clase con el constructor
predeterminado sin tener que configurar propiedades. Pero el enlace de modelos establece
parámetros string en null .
Tipos que aceptan valores NULL: estos tipos se establecen en null . En el ejemplo anterior,
el enlace de modelos establece id en null ya que es de tipo int? .
Tipos de valor: los tipos de valor que no aceptan valores NULL de tipo T se establecen en
default(T) . Por ejemplo, el enlace de modelos establecerá un parámetro int id en 0.
Considere la posibilidad de usar la validación de modelos o los tipos que aceptan valores
NULL en lugar de depender de valores predeterminados.
Si se produce un error de enlace, MVC no genera un error. Todas las acciones que aceptan datos
del usuario deben comprobar la propiedad ModelState.IsValid .
Nota: Cada entrada en la propiedad ModelState del controlador es un elemento ModelStateEntry
que contiene una propiedad Errors . No suele ser necesario consultar esta colección por su cuenta.
Utilice ModelState.IsValid en su lugar.
Además, hay algunos tipos de datos especiales que MVC debe tener en cuenta al realizar el enlace
de modelos:
IFormFile , IEnumerable<IFormFile> : uno o más archivos cargados que forman parte de la
solicitud HTTP.
CancellationToken : se usa para cancelar la actividad en controladores asincrónicos.

Estos tipos se pueden enlazar a parámetros de acción o a propiedades en un tipo de clase.


Cuando se completa el enlace de modelos, se produce la validación. El enlace de modelos
predeterminado funciona muy bien para la amplia mayoría de escenarios de desarrollo. También es
extensible, por lo que si tiene necesidades únicas, puede personalizar el comportamiento integrado.

Personalizar el comportamiento de enlace de modelos con


atributos
MVC contiene varios atributos que puede usar para dirigir su comportamiento de enlace de
modelos predeterminado a un origen diferente. Por ejemplo, puede especificar si se requiere el
enlace para una propiedad o si nunca debe ocurrir en absoluto mediante el uso de los atributos
[BindRequired] o [BindNever] . Como alternativa, puede reemplazar el origen de datos
predeterminado y especificar el origen de datos del enlazador de modelos. En esta lista se
muestran los atributos del enlace de modelos:
[BindRequired] : este atributo agrega un error al estado de modelo si no se puede producir
el enlace.
[BindNever] : indica al enlazador de modelos que nunca enlace a este parámetro.
[FromHeader] , [FromQuery] , [FromRoute] , [FromForm] : use estos para especificar el origen
de enlace exacto que quiere aplicar.
[FromServices] : este atributo usa la inserción de dependencias para enlazar parámetros de
los servicios.
[FromBody] : use los formateadores configurados para enlazar datos desde el cuerpo de
solicitud. El formateador se selecciona según el tipo de contenido de la solicitud.
[ModelBinder] : se usa para reemplazar el enlazador de modelos predeterminado, el origen
del enlace y el nombre.
Los atributos son herramientas muy útiles cuando es necesario invalidar el comportamiento
predeterminado del enlace de modelos.

Enlazar datos con formato desde el cuerpo de la solicitud


Los datos de la solicitud pueden proceder de una variedad de formatos como JSON, XML y
muchos otros. Cuando se usa el atributo [FromBody] para indicar que quiere enlazar un parámetro
a los datos en el cuerpo de la solicitud, MVC usa un conjunto de formateadores configurado para
administrar los datos de la solicitud en función de su tipo de contenido. De forma predeterminada,
MVC incluye una clase JsonInputFormatter para controlar datos JSON, pero puede agregar
formateadores adicionales para administrar XML y otros formatos personalizados.

NOTE
Puede haber como máximo un parámetro por cada acción decorada con [FromBody] . El tiempo de
ejecución de ASP.NET Core MVC delega la responsabilidad de leer la secuencia de solicitudes al formateador.
Una vez que se lee la secuencia de solicitudes para un parámetro, por lo general no es posible leer la
secuencia de solicitudes de nuevo para enlazar otros parámetros [FromBody] .
NOTE
JsonInputFormatter es el formateador predeterminado y se basa en Json.NET.

ASP.NET selecciona formateadores de entradas basándose en el encabezado Content-Type y el tipo


del parámetro, a menos que haya un atributo aplicado a él que especifique lo contrario. Si quiere
usar XML u otro formato, debe configurarlo en el archivo Startup.cs, pero primero tiene que
obtener una referencia a Microsoft.AspNetCore.Mvc.Formatters.Xml mediante NuGet. El código de
inicio debe tener este aspecto:

public void ConfigureServices(IServiceCollection services)


{
services.AddMvc()
.AddXmlSerializerFormatters();
}

El código del archivo Startup.cs contiene un método ConfigureServices con un argumento


services que puede usar para crear servicios para la aplicación ASP.NET. En el ejemplo, vamos a
agregar un formateador XML como un servicio que MVC proporcionará para esta aplicación. El
argumento options pasado al método AddMvc permite agregar y administrar filtros,
formateadores y otras opciones del sistema desde MVC al inicio de la aplicación. Después, aplique
el atributo Consumes a las clases de controlador o a los métodos de acción para que funcione con el
formato que quiera.
Enlace de modelos personalizado
Puede ampliar el enlace de modelos escribiendo sus propios enlazadores de modelos
personalizados. Más información sobre el enlace de modelos personalizados.
Validación de modelos en ASP.NET Core MVC
14/05/2018 • 29 minutes to read • Edit Online

Por Rachel Appel

Introducción a la validación de modelos


Antes de que una aplicación almacene datos en una base de datos, dicha aplicación debe validar los
datos. Es necesario comprobar los datos para detectar posibles amenazas de seguridad, verificar que
su formato es adecuado para el tipo y el tamaño y que se ajustan a las reglas. La validación es
necesaria aunque su implementación puede resultar tediosa y redundante. En MVC, la validación se
produce en el cliente y el servidor.
Por suerte, .NET ha abstraído la validación en atributos de validación. Estos atributos contienen el
código de validación, lo que reduce la cantidad de código que debe escribirse.
Vea o descargue el ejemplo de GitHub.

Atributos de validación
Los atributos de validación son una forma de configurar la validación del modelo, por lo que
conceptualmente es similar a la validación en campos de tablas de base de datos. Esto incluye las
restricciones, como la asignación de tipos de datos o los campos obligatorios. Entre otros tipos de
validación se encuentran el de aplicar patrones a datos para aplicar reglas de negocio, como tarjetas
de crédito, números de teléfono o direcciones de correo electrónico. Con los atributos de validación,
es mucho más sencillo aplicar estos requisitos y son más fáciles de usar.
Aquí mostramos un modelo Movie anotado desde una aplicación que almacena información sobre
películas y programas de TV. La mayoría de las propiedades son obligatorias y varias propiedades de
cadena tienen requisitos de longitud. Además, hay una restricción de rango numérico para la
propiedad Price de 0 a 999,99 $, junto con un atributo de validación personalizado.
public class Movie
{
public int Id { get; set; }

[Required]
[StringLength(100)]
public string Title { get; set; }

[ClassicMovie(1960)]
[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }

[Required]
[StringLength(1000)]
public string Description { get; set; }

[Range(0, 999.99)]
public decimal Price { get; set; }

[Required]
public Genre Genre { get; set; }

public bool Preorder { get; set; }


}

Con solo leer el modelo se pueden detectar las reglas sobre los datos para esta aplicación, lo que
facilita mantener el código. Aquí tenemos varios atributos de validación integradas conocidos:
[CreditCard] : valida que la propiedad tiene formato de tarjeta de crédito.
[Compare] : valida dos propiedades en una coincidencia de modelos.
[EmailAddress] : valida que la propiedad tiene formato de correo electrónico.
[Phone] : valida que la propiedad tiene formato de teléfono.
[Range] : valida que el valor de propiedad se encuentra dentro del intervalo especificado.
[RegularExpression] : valida que los datos coinciden con la expresión regular especificada.
[Required] : hace que una propiedad sea obligatoria.
[StringLength] : valida que una propiedad de cadena tiene como mucho la longitud máxima
determinada.
[Url] : valida que la propiedad tiene un formato de URL.

MVC admite cualquier atributo que se derive de ValidationAttribute a efectos de validación. En el


espacio de nombres System.ComponentModel.DataAnnotations pueden encontrarse muchos
atributos de validación útiles.
Puede haber instancias en las que necesite más características de las que proporcionan los atributos
integrados. En estos casos, puede crear atributos de validación personalizados derivando a partir de
ValidationAttribute o cambiando el modelo para implementar IValidatableObject .

Notas sobre el uso del atributo Required


Los tipos de valor que no aceptan valores NULL (como decimal , int , float y DateTime ) son
intrínsecamente necesarios y no necesitan el atributo Required . La aplicación no realiza ninguna
comprobación de validación del lado servidor para los tipos que no aceptan valores NULL que están
marcados como Required .
El enlace de modelos de MVC, que no está relacionado con la validación y los atributos de validación,
rechaza el envío de un campo de formulario que contenga un valor que falta o un espacio en blanco
para un tipo que no acepta valores NULL. En ausencia de un atributo BindRequired en la propiedad
de destino, el enlace de modelos omite datos que faltan para los tipos que no aceptan valores NULL,
donde el campo de formulario está ausente en los datos entrantes del formulario.
El atributo BindRequired (vea también Personalizar el comportamiento de enlace de modelo con
atributos) es útil para garantizar que los datos de formulario se completan. Cuando se aplica a una
propiedad, el sistema de enlace de modelos requiere un valor para esa propiedad. Cuando se aplica a
un tipo, el sistema de enlace de modelos requiere valores para todas las propiedades de ese tipo.
Cuando se usa un tipo Nullable<T > (por ejemplo, decimal? o System.Nullable<decimal> ) y se marca
como Required , se realiza una comprobación de validación del lado servidor como si la propiedad
fuera un tipo que acepta valores NULL estándar (por ejemplo, un string ).
La validación del lado cliente requiere un valor para un campo de formulario que se corresponda con
una propiedad del modelo que se haya marcado como Required y para una propiedad de tipo que
no acepta valores NULL que no ha marcado como Required . Required puede usarse para controlar
el mensaje de error de validación del lado cliente.

Estado del modelo


El estado del modelo representa los errores de validación en valores de formulario HTML enviados.
MVC seguirá validando campos hasta que alcance el número máximo de errores (200 de forma
predeterminada). Puede configurar este número si inserta el código siguiente en el método
ConfigureServices dentro del archivo Startup.cs:

services.AddMvc(options => options.MaxModelValidationErrors = 50);

Control de errores de estado del modelo


La validación de modelos se produce antes de invocar cada acción de controlador, y es el método de
acción el encargado de inspeccionar ModelState.IsValid y reaccionar de manera apropiada. En
muchos casos, la reacción adecuada consiste en devolver una respuesta de error. Lo ideal sería que
detallara el motivo por el motivo del error de validación del modelo.
Algunas aplicaciones optarán por seguir una convención estándar para tratar los errores de
validación de modelos, en cuyo caso un filtro podría ser el lugar adecuado para implementar esta
directiva. Debe probar cómo se comportan las acciones con estados de modelo válidos y no válidos.

Validación manual
Después de completarse la validación y el enlace de modelos, es posible que quiera repetir partes de
estos procesos. Por ejemplo, puede que un usuario haya escrito texto en un campo esperando recibir
un entero, o puede que necesite calcular un valor para la propiedad del modelo.
Es posible que tenga que ejecutar manualmente la validación. Para ello, llame al método
TryValidateModel , como se muestra aquí:

TryValidateModel(movie);
Validación personalizada
Los atributos de validación funcionan para la mayoría de las necesidades validación. Pero algunas
reglas de validación son específicas de su negocio. Es posible que las reglas no sean técnicas de
validación de datos comunes, como asegurarse de que un campo es obligatorio o se ajusta a un
rango de valores. Para estos escenarios, los atributos de validación personalizados son una excelente
solución. Es muy fácil crear sus propios atributos de validación personalizados en MVC. Solo tiene
que heredar de ValidationAttribute e invalidar el método IsValid . El método IsValid acepta dos
parámetros: el primero es un objeto denominado value y el segundo es un objeto ValidationContext
denominado validationContext. El objeto value hace referencia al valor real del campo que va a
validar el validador personalizado.
En el ejemplo siguiente, una regla de negocio indica que los usuarios no pueden especificar el género
Classic para una película estrenada después de 1960. El atributo [ClassicMovie] comprueba primero
el género y, si es un clásico, comprueba que la fecha de estreno es posterior a 1960. Si se estrenó
después de 1960, se produce un error de validación. El atributo acepta un parámetro de entero que
representa el año que puede usar para validar los datos. Puede capturar el valor del parámetro en el
constructor del atributo, como se muestra aquí:

public class ClassicMovieAttribute : ValidationAttribute, IClientModelValidator


{
private int _year;

public ClassicMovieAttribute(int Year)


{
_year = Year;
}

protected override ValidationResult IsValid(object value, ValidationContext


validationContext)
{
Movie movie = (Movie)validationContext.ObjectInstance;

if (movie.Genre == Genre.Classic && movie.ReleaseDate.Year > _year)


{
return new ValidationResult(GetErrorMessage());
}

return ValidationResult.Success;
}

La variable movie anterior representa un objeto Movie que contiene los datos del envío del
formulario para validarlos. En este caso, el código de validación comprueba la fecha y el género en el
método IsValid de la clase ClassicMovieAttribute según las reglas. Después de validar
correctamente IsValid , devuelve un código ValidationResult.Success y, cuando se produce un error
de validación, un ValidationResult con un mensaje de error. Cuando un usuario modifica el campo
Genre y envía el formulario, el método IsValid de ClassicMovieAttribute comprueba si la película
es un clásico. Al igual que con cualquier atributo integrado, aplique ClassicMovieAttribute a una
propiedad como ReleaseDate para asegurarse de que se produce la validación, tal como se muestra
en el ejemplo de código anterior. Puesto que el ejemplo solo funciona con tipos Movie , una mejor
opción es usar IValidatableObject tal y como se muestra en el párrafo siguiente.
Si lo prefiere, puede colocar este mismo código en el modelo, implementando el método Validate
en la interfaz IValidatableObject . Aunque los atributos de validación personalizada funcionan bien
para validar propiedades individuales, puede implementar IValidatableObject para implementar la
validación de nivel de clase como se ve aquí.
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (Genre == Genre.Classic && ReleaseDate.Year > _classicYear)
{
yield return new ValidationResult(
$"Classic movies must have a release year earlier than {_classicYear}.",
new[] { "ReleaseDate" });
}
}

Validación del lado cliente


La validación del lado cliente es una gran ventaja para los usuarios. Permite ahorrar tiempo que, de lo
contrario, pasarían esperando un recorrido de ida y vuelta al servidor. En términos comerciales,
incluso unas pocas fracciones de segundos multiplicadas por cientos de veces al día suman una gran
cantidad de tiempo, gastos y frustración. La validación inmediata y sencilla permite a los usuarios
trabajar de forma más eficiente y generar una entrada y salida de datos de mayor calidad.
Debe tener una vista con las referencias de script de JavaScript adecuadas para que la validación del
lado cliente funcione como se ve aquí.

<script src="https://ajax.aspnetcdn.com/ajax/jQuery/jquery-2.2.0.min.js"></script>

<script src="https://ajax.aspnetcdn.com/ajax/jquery.validate/1.16.0/jquery.validate.min.js">
</script>
<script
src="https://ajax.aspnetcdn.com/ajax/jquery.validation.unobtrusive/3.2.6/jquery.validate.unobtrus
ive.min.js"></script>

El script Validación discreta de jQuery es una biblioteca front-end personalizada de Microsoft que se
basa en el conocido complemento Validación de jQuery. Si no usa Validación discreta de jQuery,
deberá codificar la misma lógica de validación dos veces: una vez en los atributos de validación del
lado servidor en las propiedades del modelo y luego en los scripts del lado cliente (los ejemplos del
método validate() de Validación de jQuery muestran lo complejo que podría resultar). En su lugar,
las aplicaciones auxiliares de etiquetas y las aplicaciones auxiliares de HTML de MVC pueden usar
los atributos de validación y escribir metadatos de las propiedades del modelo para representar
atributos de datos HTML 5 en los elementos de formulario que necesitan validación. MVC genera los
atributos data- para los atributos integrados y los personalizados. La Validación discreta de jQuery
analiza después los atributos data- y pasa la lógica a Validación de jQuery. De este modo, la lógica
de validación del lado servidor se "copia" de manera eficaz en el cliente. Puede mostrar errores de
validación en el cliente usando las aplicaciones auxiliares de etiquetas relevantes, como se muestra
aquí:

<div class="form-group">
<label asp-for="ReleaseDate" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="ReleaseDate" class="form-control" />
<span asp-validation-for="ReleaseDate" class="text-danger"></span>
</div>
</div>

Las anteriores aplicaciones auxiliares de etiqueta representan el código HTML siguiente. Tenga en
cuenta que los atributos data- en los resultados HTML corresponden a los atributos de validación
para la propiedad ReleaseDate . El atributo data-val-required siguiente contiene un mensaje de
error que se muestra si el usuario no rellena el campo de fecha de estreno. La Validación discreta de
jQuery pasa este valor al método required() de la Validación de jQuery, que muestra un mensaje en
el elemento <span> que lo acompaña.

<form action="/Movies/Create" method="post">


<div class="form-horizontal">
<h4>Movie</h4>
<div class="text-danger"></div>
<div class="form-group">
<label class="col-md-2 control-label" for="ReleaseDate">ReleaseDate</label>
<div class="col-md-10">
<input class="form-control" type="datetime"
data-val="true" data-val-required="The ReleaseDate field is required."
id="ReleaseDate" name="ReleaseDate" value="" />
<span class="text-danger field-validation-valid"
data-valmsg-for="ReleaseDate" data-valmsg-replace="true"></span>
</div>
</div>
</div>
</form>

La validación del lado cliente impide realizar el envío hasta que el formulario sea válido. El botón
Enviar ejecuta JavaScript para enviar el formulario o mostrar mensajes de error.
MVC determina los valores de atributo de tipo según el tipo de datos de .NET de una propiedad,
posiblemente invalidada usando los atributos [DataType] . El atributo [DataType] de base no realiza
ninguna validación en el lado servidor. Los exploradores eligen sus propios mensajes de error y
muestran estos errores cuando quieren, si bien el paquete Validación discreta de jQuery puede
invalidar esos mensajes y mostrarlos de manera coherente con otros. Esto es más evidente cuando
los usuarios aplican subclases de [DataType] , como por ejemplo, [EmailAddress] .
Agregar validación a formularios dinámicos
Como la Validación discreta de jQuery pasa la lógica de validación a Validación de jQuery cuando la
página se carga por primera vez, los formularios generados dinámicamente no exhiben la validación
automáticamente. En su lugar, hay que indicar a Validación discreta de jQuery que analice el
formulario dinámico inmediatamente después de crearlo. Por ejemplo, en este código se muestra
cómo es posible configurar la validación del lado cliente en un formulario agregado mediante AJAX.

$.get({
url: "https://url/that/returns/a/form",
dataType: "html",
error: function(jqXHR, textStatus, errorThrown) {
alert(textStatus + ": Couldn't add form. " + errorThrown);
},
success: function(newFormHTML) {
var container = document.getElementById("form-container");
container.insertAdjacentHTML("beforeend", newFormHTML);
var forms = container.getElementsByTagName("form");
var newForm = forms[forms.length - 1];
$.validator.unobtrusive.parse(newForm);
}
})

El método $.validator.unobtrusive.parse() acepta un selector de jQuery para su único argumento.


Este método indica a Validación discreta de jQuery que analice los atributos data- de formularios
dentro de ese selector. Los valores de estos atributos se pasan al complemento Validación de jQuery
para que el formulario muestre las reglas de validación del lado cliente que quiera.
Agregar validación a controles dinámicos
También puede actualizar las reglas de validación en un formulario cuando se generan
dinámicamente controles individuales, como <input/> s y <select/> s. No se pueden pasar
directamente selectores para estos elementos al método parse() porque el formulario adyacente ya
se ha analizado y no se actualiza. En lugar de esto, quite primero los datos de validación existentes y
vuelva a analizar todo el formulario, como se muestra aquí:

$.get({
url: "https://url/that/returns/a/control",
dataType: "html",
error: function(jqXHR, textStatus, errorThrown) {
alert(textStatus + ": Couldn't add control. " + errorThrown);
},
success: function(newInputHTML) {
var form = document.getElementById("my-form");
form.insertAdjacentHTML("beforeend", newInputHTML);
$(form).removeData("validator") // Added by jQuery Validate
.removeData("unobtrusiveValidation"); // Added by jQuery Unobtrusive Validation
$.validator.unobtrusive.parse(form);
}
})

IClientModelValidator
Puede crear lógica del lado cliente para el atributo personalizado y Validación discreta, que crea un
adaptador para la validación de jQuery, la ejecutará en el cliente automáticamente como parte de la
validación. El primer paso consiste en controlar qué atributos de datos se agregan mediante la
implementación de la interfaz IClientModelValidator , como se muestra aquí:

public void AddValidation(ClientModelValidationContext context)


{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}

MergeAttribute(context.Attributes, "data-val", "true");


MergeAttribute(context.Attributes, "data-val-classicmovie", GetErrorMessage());

var year = _year.ToString(CultureInfo.InvariantCulture);


MergeAttribute(context.Attributes, "data-val-classicmovie-year", year);
}

Los atributos que implementan esta interfaz pueden agregar atributos HTML a los campos
generados. Al examinar la salida del elemento ReleaseDate muestra que el HTML es similar al
ejemplo anterior, excepto que ahora hay un atributo data-val-classicmovie que se definió en el
método AddValidation de IClientModelValidator .

<input class="form-control" type="datetime"


data-val="true"
data-val-classicmovie="Classic movies must have a release year earlier than 1960."
data-val-classicmovie-year="1960"
data-val-required="The ReleaseDate field is required."
id="ReleaseDate" name="ReleaseDate" value="" />

La validación discreta usa los datos en los atributos data- para mostrar mensajes de error. Pero
jQuery no sabe nada de reglas o mensajes hasta que estas se agregan al objeto validator de
jQuery. Esto se muestra en el ejemplo siguiente, que agrega un método denominado classicmovie
que contiene código de validación de cliente personalizada para el objeto validator de jQuery. Aquí
encontrará una explicación del método unobtrusive.adapters.add.

$(function () {
$.validator.addMethod('classicmovie',
function (value, element, params) {
// Get element value. Classic genre has value '0'.
var genre = $(params[0]).val(),
year = params[1],
date = new Date(value);
if (genre && genre.length > 0 && genre[0] === '0') {
// Since this is a classic movie, invalid if release date is after given year.
return date.getFullYear() <= year;
}

return true;
});

$.validator.unobtrusive.adapters.add('classicmovie',
['year'],
function (options) {
var element = $(options.form).find('select#Genre')[0];
options.rules['classicmovie'] = [element, parseInt(options.params['year'])];
options.messages['classicmovie'] = options.message;
});
});

Ahora jQuery tiene información para ejecutar la validación personalizada de JavaScript, así como el
mensaje de error que se debe mostrar si ese código de validación devuelve false.

Validación remota
La validación remota es una característica excelente que se usa cuando necesita validar los datos del
cliente con datos del servidor. Por ejemplo, puede que la aplicación necesite comprobar si ya se está
usando un nombre de usuario o una dirección de correo, y debe consultar una gran cantidad de
datos para hacerlo. El hecho de descargar grandes conjuntos de datos para validar un campo o unos
pocos campos consume demasiados recursos. También podría exponer información confidencial.
Una alternativa a esto consiste en realizar una solicitud de ida y vuelta para validar un campo.
Puede implementar la validación remota en un proceso de dos pasos. Primero, debe anotar el
modelo con el atributo [Remote] . El atributo [Remote] acepta varias sobrecargas que puede usar
para dirigir el JavaScript del lado cliente al código adecuado al que se debe llamar. El ejemplo
siguiente señala al método de acción VerifyEmail del controlador Users .

[Remote(action: "VerifyEmail", controller: "Users")]


public string Email { get; set; }

El segundo paso consiste en colocar el código de validación en el método de acción correspondiente,


tal como se define en el atributo [Remote] . Según la documentación del método remote() de
Validación de jQuery:

La respuesta del lado servidor debe ser una cadena JSON que debe ser "true" para elementos
válidos y puede ser "false" , undefined o null para elementos no válidos, usando el mensaje
de error predeterminado. Si la respuesta del lado servidor es una cadena, como por ejemplo,
"That name is already taken, try peter123 instead" , dicha cadena se mostrará como un mensaje
de error personalizado en lugar del predeterminado.
La definición del método VerifyEmail() sigue estas reglas, tal y como se muestra abajo. Devuelve un
mensaje de error de validación si la dirección de correo ya existe o muestra true si la dirección de
correo está disponible, y ajusta el resultado en un objeto JsonResult . Después, el lado cliente puede
usar el valor devuelto para continuar o mostrar el error si es necesario.

[AcceptVerbs("Get", "Post")]
public IActionResult VerifyEmail(string email)
{
if (!_userRepository.VerifyEmail(email))
{
return Json($"Email {email} is already in use.");
}

return Json(true);
}

Cuando los usuarios escriben una dirección de correo, JavaScript en la vista hace una llamada
remota para comprobar si esa dirección ya existe y, de ser así, se muestra el mensaje de error. En caso
contrario, el usuario puede enviar el formulario como de costumbre.
La propiedad AdditionalFields del atributo [Remote] es útil para validar combinaciones de campos
con los datos del servidor. Por ejemplo, si el modelo User anterior tenía dos propiedades adicionales
llamadas FirstName y LastName , recomendamos comprobar que ningún usuario actual tenga ya ese
par de nombres. Tendrá que definir las nuevas propiedades como se muestra en el código siguiente:

[Remote(action: "VerifyName", controller: "Users", AdditionalFields = nameof(LastName))]


public string FirstName { get; set; }
[Remote(action: "VerifyName", controller: "Users", AdditionalFields = nameof(FirstName))]
public string LastName { get; set; }

AdditionalFields podría haberse configurado explícitamente para las cadenas "FirstName" y


"LastName" , pero al usar el operador nameof de este modo se simplifica la refactorización posterior.
El método de acción para realizar la validación debe aceptar dos argumentos, uno para el valor de
FirstName y otro para el valor de LastName .

[AcceptVerbs("Get", "Post")]
public IActionResult VerifyName(string firstName, string lastName)
{
if (!_userRepository.VerifyName(firstName, lastName))
{
return Json(data: $"A user named {firstName} {lastName} already exists.");
}

return Json(data: true);


}

Cuando los usuarios escriben su nombre y sus apellidos, JavaScript hace lo siguiente:
Realiza una llamada remota para comprobar si ese par de nombres ya se está usando.
Si el par de nombres ya existe, se muestra un mensaje de error.
Si está disponible, el usuario puede enviar el formulario.
Si necesita validar dos o más campos adicionales con el atributo [Remote] , proporciónelos como una
lista delimitada por comas. Por ejemplo, para agregar una MiddleName propiedad en el modelo,
establezca el [Remote] atributo tal como se muestra en el código siguiente:
[Remote(action: "VerifyName", controller: "Users", AdditionalFields = nameof(FirstName) + "," +
nameof(LastName))]
public string MiddleName { get; set; }

AdditionalFields , al igual que todos los argumentos de atributo, debe ser una expresión constante.
Por lo tanto, no se debe usar una cadena interpolada ni llamar a string.Join() para inicializar
AdditionalFields . Para cada campo adicional que agregue al atributo [Remote] , debe agregar otro
argumento al método de acción de controlador correspondiente.
Vistas de ASP.NET Core MVC
25/06/2018 • 27 minutes to read • Edit Online

Por Steve Smith y Luke Latham


En este documento se explican las vistas utilizadas en las aplicaciones de ASP.NET Core MVC. Para obtener
información sobre las páginas de Razor, consulte Introducción a las páginas Razor.
En el patrón de controlador de vista de modelos (MVC ), la vista se encarga de la presentación de los datos y de
la interacción del usuario. Una vista es una plantilla HTML con marcado de Razor insertado. El marcado de
Razor es código que interactúa con el formato HTML para generar una página web que se envía al cliente.
En ASP.NET Core MVC, las vistas son archivos .cshtml que usan el lenguaje de programación C# en el marcado
de Razor. Por lo general, los archivos de vistas se agrupan en carpetas con el nombre de cada uno de los
controladores de la aplicación. Las carpetas se almacenan en una carpeta llamada Views que está ubicada en la
raíz de la aplicación:

El controlador Home está representado por una carpeta Home situada dentro de la carpeta Views. La carpeta
Home contiene las vistas correspondientes a las páginas web About, Contact e Index (página principal). Cuando
un usuario solicita una de estas tres páginas web, las acciones del controlador Home determinan cuál de las tres
vistas se usa para crear y devolver una página web al usuario.
Use diseños para cohesionar las secciones de la página web y reducir la repetición del código. Los diseños
suelen contener encabezado, elementos de menú y navegación y pie de página. El encabezado y el pie de página
suelen contener marcado reutilizable para muchos elementos de metadatos y vínculos a recursos de script y
estilo. Los diseños ayudan a evitar este marcado reutilizable en las vistas.
Las vistas parciales administran las partes reutilizables de las vistas para reducir la duplicación de código. Por
ejemplo, resulta útil usar una vista parcial en la biografía de un autor que aparece en varias vistas de un sitio
web de blog. Una biografía de autor es contenido de vista normal y no requiere que se ejecute código para
generar el contenido de la página web. El contenido de la biografía del autor está disponible para la vista
simplemente mediante un enlace de modelos, por lo que resulta ideal usar una vista parcial para este tipo de
contenido.
Los componentes de la vista se asemejan a las vistas parciales en que permiten reducir el código repetitivo, pero
son adecuados para ver contenido que requiere la ejecución de código en el servidor con el fin de representar la
página web. Los componentes de la vista son útiles cuando el contenido representado requiere una interacción
con la base de datos, por ejemplo, en el carro de la compra de un sitio web. Los componentes de la vista no se
limitan a un enlace de modelos para generar la salida de la página web.
Ventajas de utilizar las vistas
Las vistas separan el marcado de la interfaz de usuario de otras partes de la aplicación con el fin de ayudar a
establecer una separación de intereses (SoC ) dentro de una aplicación MVC. Diseñar respetando el principio
SoC hace que la aplicación sea modular, lo que ofrece varias ventajas:
La aplicación es más fácil de mantener, ya que está mejor organizada. Las vistas generalmente se agrupan
por característica de la aplicación. Esto facilita la búsqueda de vistas relacionadas cuando se trabaja en una
característica.
Las partes de la aplicación están acopladas de forma ligera. Las vistas de la aplicación se pueden compilar y
actualizar por separado de los componentes de la lógica de negocios y el acceso a datos. Las vistas de la
aplicación se pueden modificar sin necesidad de tener que actualizar otras partes de la aplicación.
Es más fácil probar los elementos de la interfaz de usuario de la aplicación, ya que las vistas son unidades
independientes.
Debido a una mejor organización, es menos probable que se repitan accidentalmente secciones de la interfaz
de usuario.

Creación de una vista


Las vistas que son específicas de un controlador se crean en la carpeta Views/[nombreDelControlador ]. Las
vistas compartidas entre controladores se colocan en la carpeta Views/Shared. Para crear una vista, agregue un
archivo nuevo y asígnele el mismo nombre que a la acción del controlador asociada con la extensión de archivo
.cshtml. Para crear una vista que se corresponda con la acción About del controlador Home, cree un archivo
About.cshtml en la carpeta Views/Home:

@{
ViewData["Title"] = "About";
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>

<p>Use this area to provide additional information.</p>

El marcado de Razor comienza con el símbolo @ . Ejecute instrucciones de C# mediante la colocación de código
C# en los bloques de código Razor activados por llaves ( { ... } ). Por ejemplo, vea la asignación de "About" en
ViewData["Title"] mostrada anteriormente. Para mostrar valores en HTML, simplemente haga referencia al
valor con el símbolo @ . Ver el contenido de los elementos <h2> y <h3> anteriores.
El contenido de la vista mostrado anteriormente es solo una parte de toda la página web que se presenta al
usuario. El resto del diseño de la página y otros aspectos comunes de la vista se especifican en otros archivos de
vista. Para obtener más información, consulte el tema Diseño.

Cómo especifican los controladores las vistas


Las vistas normalmente las devuelven acciones como ViewResult, que es un tipo de ActionResult. El método de
acción puede crear y devolver ViewResult directamente, pero no es lo más común. Puesto que la mayoría de los
controladores heredan de Controller, simplemente se usa el método auxiliar View para devolver ViewResult :
HomeController.cs
public IActionResult About()
{
ViewData["Message"] = "Your application description page.";

return View();
}

Cuando esta acción devuelve un resultado, la vista About.cshtml mostrada en la última sección se representa
como la página web siguiente:

El método auxiliar View tiene varias sobrecargas. También puede especificar:


Una vista explícita para devolver:

return View("Orders");

Un modelo para pasar a la vista:

return View(Orders);

Una vista y un modelo:

return View("Orders", Orders);

Detección de vista
Cuando una acción devuelve una vista, tiene lugar un proceso llamado detección de vista. Este proceso
determina qué archivo de vista se utiliza en función del nombre de la vista.
El comportamiento predeterminado del método View ( return View(); ) es devolver una vista con el mismo
nombre que el método de acción desde el que se llama. Por ejemplo, el nombre de método About ActionResult
del controlador se usa para buscar un archivo de vista denominado About.cshtml. En primer lugar, el runtime
busca la vista en la carpeta Views/[nombreDelControlador ]. Si no encuentra una vista que coincida, busca la
vista en la carpeta Shared.
Da igual si se devuelve implícitamente ViewResult con return View(); o si se pasa explícitamente el nombre
de la vista al método View con return View("<ViewName>"); . En ambos casos, la detección de vista busca un
archivo de vista coincidente en este orden:
1. Views/[nombreDeControlador ]/[nombreDeVista ].cshtml
2. Views/Shared/[nombreDeVista ].cshtml
En lugar del nombre de una vista, se puede proporcionar la ruta de acceso del archivo de vista. Si se utiliza una
ruta de acceso absoluta que comience en la raíz de la aplicación (también puede empezar con "/" o "/"), debe
especificarse la extensión .cshtml:

return View("Views/Home/About.cshtml");

También se puede usar una ruta de acceso relativa para especificar vistas de directorios distintos sin la extensión
.cshtml. Dentro de HomeController , se puede devolver la vista Index de las vistas Manage con una ruta de
acceso relativa:

return View("../Manage/Index");

De forma similar, se puede indicar el directorio específico del controlador actual con el prefijo "./":

return View("./About");

Las vistas parciales y los componentes de vista usan mecanismos de detección similares (aunque no idénticos).
Puede personalizar la convención predeterminada para la localización de las vistas en la aplicación si utiliza una
interfaz IViewLocationExpander personalizada.
La detección de vistas se basa en la búsqueda de archivos de vista por nombre de archivo. Si el sistema de
archivos subyacente distingue mayúsculas de minúsculas, es probable que también lo hagan los nombres de las
vistas. Para que haya compatibilidad entre sistemas operativos, haga coincidir las letras mayúsculas y
minúsculas de los nombres de controlador y acción con los nombres de archivo y carpetas de vista asociados. Si
mientras trabaja con un sistema de archivos que distingue mayúsculas de minúsculas se produce un error que
le indica que no se encuentra un archivo de vista, confirme que el archivo de vista solicitado y el nombre de
archivo de vista real coinciden en el uso de mayúsculas.
Siga el procedimiento recomendado de organizar la estructura de archivos de vistas de modo que refleje las
relaciones existentes entre controladores, acciones y vistas para una mayor claridad y un mantenimiento más
sencillo.

Paso de datos a las vistas


Se pueden pasar datos a vistas con varios métodos:
Datos fuertemente tipados: ViewModel
Datos con establecimiento flexible de tipos
ViewData ( ViewDataAttribute )
ViewBag

Datos fuertemente tipados (ViewModel)


El enfoque más eficaz consiste en especificar un tipo de modelo en la vista. Este modelo se conoce normalmente
como viewmodel (modelo de vista) y en él se pasa una instancia de tipo viewmodel a la vista de la acción.
La utilización de un modelo de vista para pasar datos a una vista permite que la vista se beneficie de las ventajas
de la comprobación de tipos seguros. La especificación detallada de tipos (fuertemente tipado) significa que
cada variable y cada constante tienen un tipo definido de forma explícita (por ejemplo, string , int o
DateTime ). La validez de los tipos usados en una vista se comprueba en tiempo de compilación.
Visual Studio y Visual Studio Code enumeran los miembros de clase fuertemente tipados mediante una
característica denominada IntelliSense. Si quiere ver las propiedades de un modelo de vista, escriba el nombre
de variable del modelo de vista seguido por un punto ( . ). Esto ayuda a escribir código más rápidamente y con
menos errores.
Especifique un modelo con la directiva @model . Utilice el modelo con @Model :

@model WebApplication1.ViewModels.Address

<h2>Contact</h2>
<address>
@Model.Street<br>
@Model.City, @Model.State @Model.PostalCode<br>
<abbr title="Phone">P:</abbr> 425.555.0100
</address>

Para proporcionar el modelo a la vista, el controlador lo pasa como un parámetro:

public IActionResult Contact()


{
ViewData["Message"] = "Your contact page.";

var viewModel = new Address()


{
Name = "Microsoft",
Street = "One Microsoft Way",
City = "Redmond",
State = "WA",
PostalCode = "98052-6399"
};

return View(viewModel);
}

No hay ninguna restricción sobre los tipos de modelo que se pueden proporcionar a una vista. Se recomienda
usar modelos de vista POCO (objeto CRL estándar) con poco o ningún comportamiento (métodos) definido.
Por lo general, las clases de modelo de vista se almacenan en la carpeta Models o en otra carpeta ViewModels
en la raíz de la aplicación. El modelo de vista Address usado en el ejemplo anterior es un modelo de vista POCO
almacenado en un archivo denominado Address.cs:

namespace WebApplication1.ViewModels
{
public class Address
{
public string Name { get; set; }
public string Street { get; set; }
public string City { get; set; }
public string State { get; set; }
public string PostalCode { get; set; }
}
}

Nada le impide usar las mismas clases tanto para los tipos de modelo de vista como para los de modelo de
negocio. Pero el uso de modelos independientes permite que las vistas varíen de forma independiente de las
partes de lógica de negocios y acceso de datos de la aplicación. La separación de los modelos y los modelos de
vista también ofrece ventajas de seguridad cuando los modelos utilizan enlace de modelo y validación para los
datos enviados a la aplicación por el usuario.
Datos con establecimiento flexible de tipos (ViewData, atributo ViewData y ViewBag)
ViewBag no está disponible en las páginas de Razor.
Además de las vistas fuertemente tipadas, las vistas tienen acceso a una colección de datos débilmente tipada
(también denominada imprecisa). A diferencia de los tipos fuertes, en los tipos débiles (o débilmente tipados) no
se declara explícitamente el tipo de datos que se está utilizando. Puede usar la colección de datos débilmente
tipada para introducir y sacar pequeñas cantidades de datos de los controladores y las vistas.

PASAR DATOS ENTRE... EJEMPLO

Un controlador y una vista Rellenar una lista desplegable con datos.

Una vista y una vista de diseño Establecer el contenido del elemento <title> en la vista de
diseño de un archivo de vista.

Una vista parcial y una vista Un widget que muestra datos basados en la página web que
el usuario solicitó.

Puede hacer referencia a esta colección a través de las propiedades ViewData o ViewBag en controladores y
vistas. La propiedad ViewData es un diccionario de objetos débilmente tipados. La propiedad ViewBag es un
contenedor alrededor de ViewData que proporciona propiedades dinámicas para la colección ViewData
subyacente.
ViewData y ViewBag se resuelven de forma dinámica en tiempo de ejecución. Debido a que no ofrecen la
comprobación de tipos en tiempo de compilación, ambas son generalmente más propensas a errores que el uso
de un modelo de vista. Por esta razón, algunos desarrolladores prefieren prescindir de ViewData y ViewBag o
usarlos lo menos posible.
ViewData
ViewData es un objeto ViewDataDictionary al que se accede a través de claves string . Los datos de cadena se
pueden almacenar y utilizar directamente sin necesidad de una conversión, pero primero debe convertir otros
valores de objeto ViewData a tipos específicos cuando los extrae. Se puede usar ViewData para pasar datos de
los controladores a las vistas y dentro de las vistas, incluidas las vistas parciales y los diseños.
En el ejemplo siguiente se usa ViewData en una acción para establecer los valores de saludo y dirección:

public IActionResult SomeAction()


{
ViewData["Greeting"] = "Hello";
ViewData["Address"] = new Address()
{
Name = "Steve",
Street = "123 Main St",
City = "Hudson",
State = "OH",
PostalCode = "44236"
};

return View();
}

Trabajar con los datos en una vista:


@{
// Since Address isn't a string, it requires a cast.
var address = ViewData["Address"] as Address;
}

@ViewData["Greeting"] World!

<address>
@address.Name<br>
@address.Street<br>
@address.City, @address.State @address.PostalCode
</address>

Atributo ViewData
Otro método en el que se usa ViewDataDictionary es ViewDataAttribute. Los valores de las propiedades de
controladores o modelos de página de Razor completadas con [ViewData] se almacenan y cargan desde el
diccionario.
En el siguiente ejemplo, el controlador Home contiene una propiedad Title completada con [ViewData] . El
método About establece el título de la vista About:

public class HomeController : Controller


{
[ViewData]
public string Title { get; set; }

public IActionResult About()


{
Title = "About Us";
ViewData["Message"] = "Your application description page.";

return View();
}
}

En la vista About, tenga acceso a la propiedad Title como propiedad de modelo:

<h1>@Model.Title</h1>

En el diseño, el título se lee desde el diccionario ViewData:

<!DOCTYPE html>
<html lang="en">
<head>
<title>@ViewData["Title"] - WebApplication</title>
...

ViewBag
ViewBag no está disponible en las páginas de Razor.
ViewBag es un objeto DynamicViewData que proporciona acceso dinámico a los objetos almacenados en
ViewData . ViewBag puede ser más cómodo de trabajar con él, ya que no requiere conversión. En el ejemplo
siguiente se muestra cómo usar ViewBag con el mismo resultado que al usar ViewData anteriormente:
public IActionResult SomeAction()
{
ViewBag.Greeting = "Hello";
ViewBag.Address = new Address()
{
Name = "Steve",
Street = "123 Main St",
City = "Hudson",
State = "OH",
PostalCode = "44236"
};

return View();
}

@ViewBag.Greeting World!

<address>
@ViewBag.Address.Name<br>
@ViewBag.Address.Street<br>
@ViewBag.Address.City, @ViewBag.Address.State @ViewBag.Address.PostalCode
</address>

Uso simultáneo de ViewData y ViewBag


ViewBag no está disponible en las páginas de Razor.
Puesto que ViewData y ViewBag hacen referencia a la misma colección ViewData subyacente, se pueden utilizar
ViewData y ViewBag , y combinarlos entre ellos al leer y escribir valores.

Establezca el título con ViewBag y la descripción con ViewData en la parte superior de una vista About.cshtml:

@{
Layout = "/Views/Shared/_Layout.cshtml";
ViewBag.Title = "About Contoso";
ViewData["Description"] = "Let us tell you about Contoso's philosophy and mission.";
}

Lea las propiedades pero invierta el uso de ViewData y ViewBag . En el archivo _Layout.cshtml, obtenga el título
con ViewData y la descripción con ViewBag :

<!DOCTYPE html>
<html lang="en">
<head>
<title>@ViewData["Title"]</title>
<meta name="description" content="@ViewBag.Description">
...

Recuerde que las cadenas no requieren una conversión para ViewData . Puede usar @ViewData["Title"] sin
conversión alguna.
Es posible utilizar ViewData y ViewBag al mismo tiempo, al igual que combinar la lectura y escritura de
propiedades. Se representa el marcado siguiente:
<!DOCTYPE html>
<html lang="en">
<head>
<title>About Contoso</title>
<meta name="description" content="Let us tell you about Contoso's philosophy and mission.">
...

Resumen de las diferencias entre ViewData y ViewBag


ViewBag no está disponible en las páginas de Razor.
ViewData
Se deriva de ViewDataDictionary, por lo que tiene propiedades de diccionario que pueden ser útiles,
como ContainsKey , Add , Remove y Clear .
Las claves del diccionario son cadenas, por lo que se permiten espacios en blanco. Ejemplo:
ViewData["Some Key With Whitespace"]
Todos los tipos excepto string deben convertirse en la vista que usa ViewData .
ViewBag
Se deriva de DynamicViewData, por lo que permite la creación de propiedades dinámicas mediante la
notación de puntos ( @ViewBag.SomeKey = <value or object> ), y no se requiere ninguna conversión. La
sintaxis de ViewBag hace que sea más rápido de agregar a controladores y vistas.
Es más sencillo comprobar si hay valores null. Ejemplo: @ViewBag.Person?.Name

Cuándo utilizar ViewData o ViewBag


ViewData y ViewBag son dos enfoques igualmente válidos para pasar pequeñas cantidades de datos entre
controladores y vistas. La elección de cuál utilizar se basa en la preferencia. Aunque se pueden combinar objetos
ViewData y ViewBag , el código resulta más fácil de leer y mantener si se utiliza un único enfoque de forma
sistemática. Ambos enfoques se resuelven dinámicamente en tiempo de ejecución y, por tanto, son propensos a
generar errores en tiempo de ejecución. Algunos equipos de desarrollo los evitan.
Vistas dinámicas
Las vistas que no declaran un tipo modelo con @model , pero que tienen una instancia de modelo que se les pasa
(por ejemplo, return View(Address); ), pueden hacer referencia a las propiedades de la instancia dinámicamente:

<address>
@Model.Street<br>
@Model.City, @Model.State @Model.PostalCode<br>
<abbr title="Phone">P:</abbr> 425.555.0100
</address>

Esta característica proporciona la flexibilidad, pero no ofrece protección de compilación ni IntelliSense. Si la


propiedad no existe, se produce un error en la generación de la página web en tiempo de ejecución.

Más características de vista


Las aplicaciones auxiliares de etiquetas permiten agregar fácilmente el comportamiento del lado servidor a las
etiquetas HTML existentes. El uso de aplicaciones auxiliares de etiquetas evita la necesidad de escribir código
personalizado o aplicaciones auxiliares en las vistas. Las aplicaciones auxiliares de etiquetas se aplican como
atributos a elementos HTML y son ignoradas por los editores que no pueden procesarlas. Esto permite editar y
representar el marcado de la vista con varias herramientas.
Hay muchas aplicaciones auxiliares de HTML integradas que permiten generar marcado HTML personalizado.
La lógica más compleja de la interfaz de usuario se puede administrar mediante componentes de vista. Los
componentes de vista proporcionan la misma SoC que los controladores y las vistas. En este sentido, pueden
eliminar la necesidad de acciones y vistas que se encargan de los datos utilizados por elementos comunes de la
interfaz de usuario.
Al igual que muchos otros aspectos de ASP.NET Core, las vistas admiten inserción de dependencias, lo que
permite que los servicios se puedan insertar en las vistas.
Referencia de sintaxis de Razor para ASP.NET Core
25/06/2018 • 22 minutes to read • Edit Online

Por Rick Anderson, Luke Latham, Taylor Mullen y Dan Vicarel


Razor es una sintaxis de marcado para insertar código basado en servidor en páginas web. La sintaxis de Razor
combina marcado de Razor, C# y HTML. Los archivos que contienen sintaxis de Razor suelen tener la extensión
de archivo .cshtml.

Representación de HTML
El lenguaje de Razor predeterminado es HTML. Representar el HTML del marcado de Razor no difiere mucho de
representar el HTML de un archivo HTML. El marcado HTML de los archivos de Razor .cshtml se representa en
el servidor sin cambios.

Sintaxis de Razor
Razor admite C# y usa el símbolo @ para realizar la transición de HTML a C#. Razor evalúa las expresiones de
C# y las representa en la salida HTML.
Cuando el símbolo @ va seguido de una palabra clave reservada de Razor, realiza una transición a un marcado
específico de Razor; en caso contrario, realiza la transición a C# simple.
Para hacer escape en un símbolo @ en el marcado de Razor, use un segundo símbolo @ :

<p>@@Username</p>

El código aparecerá en HTML con un solo símbolo @ :

<p>@Username</p>

El contenido y los atributos HTML que tienen direcciones de correo electrónico no tratan el símbolo @ como un
carácter de transición. El análisis de Razor no se detiene en las direcciones de correo electrónico del siguiente
ejemplo:

<a href="mailto:Support@contoso.com">Support@contoso.com</a>

Expresiones de Razor implícitas


Las expresiones de Razor implícitas comienzan por @ , seguido de código C#:

<p>@DateTime.Now</p>
<p>@DateTime.IsLeapYear(2016)</p>

Con la excepción de la palabra clave de C# await , las expresiones implícitas no deben contener espacios. Si la
instrucción de C# tiene un final claro, se pueden entremezclar espacios:
<p>@await DoSomething("hello", "world")</p>

Las expresiones implícitas no pueden contener tipos genéricos de C#, ya que los caracteres dentro de los
corchetes ( <> ) se interpretan como una etiqueta HTML. El siguiente código no es válido:

<p>@GenericMethod<int>()</p>

El código anterior genera un error del compilador similar a uno de los siguientes:
El elemento "int" no estaba cerrado. Todos los elementos deben ser de autocierre o tener una etiqueta de fin
coincidente.
No se puede convertir el grupo de métodos "GenericMethod" en el tipo no delegado "object". ¿Intentó
invocar el método?
Las llamadas a método genéricas deben estar incluidas en una expresión de Razor explícita o en un bloque de
código de Razor.

Expresiones de Razor explícitas


Las expresiones explícitas de Razor constan de un símbolo @ y paréntesis de apertura y de cierre. Para
representar la hora de la semana pasada, se usaría el siguiente marcado de Razor:

<p>Last week this time: @(DateTime.Now - TimeSpan.FromDays(7))</p>

El contenido que haya entre paréntesis @() se evalúa y se representa en la salida.


Por lo general, las expresiones implícitas (que explicamos en la sección anterior) no pueden contener espacios. En
el siguiente código, una semana no se resta de la hora actual:

<p>Last week: @DateTime.Now - TimeSpan.FromDays(7)</p>

El código representa el siguiente HTML:

<p>Last week: 7/7/2016 4:39:52 PM - TimeSpan.FromDays(7)</p>

Se pueden usar expresiones explícitas para concatenar texto con un resultado de la expresión:

@{
var joe = new Person("Joe", 33);
}

<p>Age@(joe.Age)</p>

Sin la expresión explícita, <p>Age@joe.Age</p> se trataría como una dirección de correo electrónico y se mostraría
como <p>Age@joe.Age</p> . Pero, si se escribe como una expresión explícita, se representa <p>Age33</p> .
Se pueden usar expresiones explícitas para representar la salida de métodos genéricos en los archivos .cshtml. En
el siguiente marcado se muestra cómo corregir el error mostrado anteriormente provocado por el uso de
corchetes en un código C# genérico. El código se escribe como una expresión explícita:

<p>@(GenericMethod<int>())</p>
Codificación de expresiones
Las expresiones de C# que se evalúan como una cadena están codificadas en HTML. Las expresiones de C# que
se evalúan como IHtmlContent se representan directamente a través de IHtmlContent.WriteTo . Las expresiones
de C# que no se evalúan como IHtmlContent se convierten en una cadena por medio de ToString y se codifican
antes de que se representen.

@("<span>Hello World</span>")

El código representa el siguiente HTML:

&lt;span&gt;Hello World&lt;/span&gt;

El HTML se muestra en el explorador como:

<span>Hello World</span>

La salida de HtmlHelper.Raw no está codificada, pero se representa como marcado HTML.

WARNING
Usar HtmlHelper.Raw en una entrada de usuario no saneada constituye un riesgo de seguridad. Dicha entrada podría
contener código JavaScript malintencionado u otras vulnerabilidades de seguridad. Sanear una entrada de usuario es
complicado. Evite usar HtmlHelper.Raw con entradas de usuario.

@Html.Raw("<span>Hello World</span>")

El código representa el siguiente HTML:

<span>Hello World</span>

Bloques de código de Razor


Los bloques de código de Razor comienzan por @ y se insertan entre {} . A diferencia de las expresiones, el
código de C# dentro de los bloques de código no se representa. Las expresiones y los bloques de código de una
vista comparten el mismo ámbito y se definen en orden:

@{
var quote = "The future depends on what you do today. - Mahatma Gandhi";
}

<p>@quote</p>

@{
quote = "Hate cannot drive out hate, only love can do that. - Martin Luther King, Jr.";
}

<p>@quote</p>

El código representa el siguiente HTML:


<p>The future depends on what you do today. - Mahatma Gandhi</p>
<p>Hate cannot drive out hate, only love can do that. - Martin Luther King, Jr.</p>

Transiciones implícitas
El lenguaje predeterminado de un bloque de código es C#, pero la página de Razor puede volver al HTML:

@{
var inCSharp = true;
<p>Now in HTML, was in C# @inCSharp</p>
}

Transición delimitada explícita


Para definir una subsección de un bloque de código que deba representar HTML, inserte los caracteres que
quiera representar entre etiquetas de Razor <text>:

@for (var i = 0; i < people.Length; i++)


{
var person = people[i];
<text>Name: @person.Name</text>
}

Emplee este método para representar HTML que no esté insertado entre etiquetas HTML. Sin una etiqueta
HTML o de Razor, se produce un error de tiempo de ejecución de Razor.
La etiqueta <text> es útil para controlar el espacio en blanco al representar el contenido:
Solo se representa el contenido entre etiquetas <text>.
En la salida HTML no hay espacios en blanco antes o después de la etiqueta <text>.
Transición de línea explícita con @:
Para representar el resto de una línea completa como HTML dentro de un bloque de código, use la sintaxis @: :

@for (var i = 0; i < people.Length; i++)


{
var person = people[i];
@:Name: @person.Name
}

Sin el carácter @: en el código, se produce un error de tiempo de ejecución de Razor.


Advertencia: Si se incluyen caracteres @ de más en un archivo de Razor, se pueden producir errores de
compilador en las instrucciones más adelante en el bloque. Estos errores de compilador pueden ser difíciles de
entender porque el error real se produce antes del error notificado. Este error es habitual después de combinar
varias expresiones implícitas/explícitas en un mismo bloque de código.

Estructuras de control
Las estructuras de control son una extensión de los bloques de código. Todos los aspectos de los bloques de
código (transición a marcado, C# en línea) son válidos también en las siguientes estructuras:
Los condicionales @if, else if, else y @switch
@if controla cuándo se ejecuta el código:
@if (value % 2 == 0)
{
<p>The value was even.</p>
}

else y else if no necesitan el símbolo @ :

@if (value % 2 == 0)
{
<p>The value was even.</p>
}
else if (value >= 1337)
{
<p>The value is large.</p>
}
else
{
<p>The value is odd and small.</p>
}

En el siguiente marcado se muestra cómo usar una instrucción switch:

@switch (value)
{
case 1:
<p>The value is 1!</p>
break;
case 1337:
<p>Your number is 1337!</p>
break;
default:
<p>Your number wasn't 1 or 1337.</p>
break;
}

@for, @foreach, @while y @do while en bucle


El HTML con plantilla se puede representar con instrucciones de control en bucle. Para representar una lista de
personas:

@{
var people = new Person[]
{
new Person("Weston", 33),
new Person("Johnathon", 41),
...
};
}

Se permiten las siguientes instrucciones en bucle:


@for

@for (var i = 0; i < people.Length; i++)


{
var person = people[i];
<p>Name: @person.Name</p>
<p>Age: @person.Age</p>
}
@foreach

@foreach (var person in people)


{
<p>Name: @person.Name</p>
<p>Age: @person.Age</p>
}

@while

@{ var i = 0; }
@while (i < people.Length)
{
var person = people[i];
<p>Name: @person.Name</p>
<p>Age: @person.Age</p>

i++;
}

@do while

@{ var i = 0; }
@do
{
var person = people[i];
<p>Name: @person.Name</p>
<p>Age: @person.Age</p>

i++;
} while (i < people.Length);

Instrucción @using compuesta


En C#, las instrucciones using se usan para garantizar que un objeto se elimina. En Razor, el mismo mecanismo
se emplea para crear aplicaciones auxiliares HTML que incluyen contenido adicional. En el siguiente código, las
aplicaciones auxiliares HTML representan una etiqueta Form con la instrucción @using :

@using (Html.BeginForm())
{
<div>
email:
<input type="email" id="Email" value="">
<button>Register</button>
</div>
}

Con las aplicaciones auxiliares de etiquetas se pueden realizar acciones de nivel de ámbito.
@try, catch, finally
El control de excepciones es similar a C#:
@try
{
throw new InvalidOperationException("You did something invalid.");
}
catch (Exception ex)
{
<p>The exception message: @ex.Message</p>
}
finally
{
<p>The finally statement.</p>
}

@lock
Razor tiene la capacidad de proteger las secciones más importantes con instrucciones de bloqueo:

@lock (SomeLock)
{
// Do critical section work
}

Comentarios
Razor admite comentarios tanto de C# como HTML:

@{
/* C# comment */
// Another C# comment
}
<!-- HTML comment -->

El código representa el siguiente HTML:

<!-- HTML comment -->

El servidor quitará los comentarios de Razor antes de mostrar la página web. Razor usa @* *@ para delimitar
comentarios. El siguiente código está comentado, de modo que el servidor no representa ningún marcado:

@*
@{
/* C# comment */
// Another C# comment
}
<!-- HTML comment -->
*@

Directivas
Las directivas de Razor se representan en las expresiones implícitas con palabras clave reservadas seguidas del
símbolo @ . Normalmente, una directiva cambia la forma en que una vista se analiza, o bien habilita una
funcionalidad diferente.
Conocer el modo en que Razor genera el código de una vista hace que sea más fácil comprender cómo
funcionan las directivas.
@{
var quote = "Getting old ain't for wimps! - Anonymous";
}

<div>Quote of the Day: @quote</div>

El código genera una clase similar a la siguiente:

public class _Views_Something_cshtml : RazorPage<dynamic>


{
public override async Task ExecuteAsync()
{
var output = "Getting old ain't for wimps! - Anonymous";

WriteLiteral("/r/n<div>Quote of the Day: ");


Write(output);
WriteLiteral("</div>");
}
}

Más adelante en este artículo, en la sección Visualización de la clase C# de Razor generada por una vista, se
explica cómo ver esta clase generada.
@using
La directiva @using agrega la directiva using de C# a la vista generada:

@using System.IO
@{
var dir = Directory.GetCurrentDirectory();
}
<p>@dir</p>

@model
La directiva @model especifica el tipo del modelo que se pasa a una vista:

@model TypeNameOfModel

En una aplicación ASP.NET Core MVC creada con cuentas de usuario individuales, la vista
Views/Account/Login.cshtml contiene la siguiente declaración de modelo:

@model LoginViewModel

La clase generada se hereda de RazorPage<dynamic> :

public class _Views_Account_Login_cshtml : RazorPage<LoginViewModel>

Razor expone una propiedad Model para tener acceso al modelo que se ha pasado a la vista:

<div>The Login Email: @Model.Email</div>

La directiva @model especifica el tipo de esta propiedad. La directiva especifica el elemento T en RazorPage<T>
de la clase generada de la que se deriva la vista. Si la directiva @model no se especifica, la propiedad Model es de
tipo dynamic . El valor del modelo se pasa del controlador a la vista. Para más información, vea Modelos
fuertemente tipados y la palabra clave @model.
@inherits
La directiva @inherits proporciona control total sobre la clase que la vista hereda:

@inherits TypeNameOfClassToInheritFrom

El siguiente código es un tipo personalizado de página de Razor:

using Microsoft.AspNetCore.Mvc.Razor;

public abstract class CustomRazorPage<TModel> : RazorPage<TModel>


{
public string CustomText { get; } = "Gardyloo! - A Scottish warning yelled from a window before dumping a
slop bucket on the street below.";
}

CustomText se muestra en una vista:

@inherits CustomRazorPage<TModel>

<div>Custom text: @CustomText</div>

El código representa el siguiente HTML:

<div>Custom text: Gardyloo! - A Scottish warning yelled from a window before dumping a slop bucket on the
street below.</div>

@model y @inherits se pueden usar en la misma vista. @inherits puede estar en un archivo
_ViewImports.cshtml que la vista importa:

@inherits CustomRazorPage<TModel>

El siguiente código es un ejemplo de una vista fuertemente tipada:

@inherits CustomRazorPage<TModel>

<div>The Login Email: @Model.Email</div>


<div>Custom text: @CustomText</div>

Si "rick@contoso.com" se pasa en el modelo, la vista genera el siguiente marcado HTML:

<div>The Login Email: rick@contoso.com</div>


<div>Custom text: Gardyloo! - A Scottish warning yelled from a window before dumping a slop bucket on the
street below.</div>

@inject
La directiva @inject permite a la página de Razor insertar un servicio del contenedor de servicios en una vista.
Para más información, vea Dependency injection into views (Inserción de dependencias en vistas).
@functions
La directiva @functions permite que una página de Razor agregue un bloque de código de C# a una vista:
@functions { // C# Code }

Por ejemplo:

@functions {
public string GetHello()
{
return "Hello";
}
}

<div>From method: @GetHello()</div>

El código genera el siguiente marcado HTML:

<div>From method: Hello</div>

El siguiente código es la clase C# de Razor generada:

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Razor;

public class _Views_Home_Test_cshtml : RazorPage<dynamic>


{
// Functions placed between here
public string GetHello()
{
return "Hello";
}
// And here.
#pragma warning disable 1998
public override async Task ExecuteAsync()
{
WriteLiteral("\r\n<div>From method: ");
Write(GetHello());
WriteLiteral("</div>\r\n");
}
#pragma warning restore 1998

@section
La directiva @section se usa junto con el diseño para permitir que las vistas representen el contenido en
diferentes partes de la página HTML. Para más información, vea Sections (Secciones).

Aplicaciones auxiliares de etiquetas


Hay tres directivas que pertenecen a las aplicaciones auxiliares de etiquetas.

DIRECTIVA FUNCIÓN

@addTagHelper Pone las aplicaciones auxiliares de etiquetas a disposición de


una vista.

@removeTagHelper Quita las aplicaciones auxiliares de etiquetas agregadas


anteriormente desde una vista.
DIRECTIVA FUNCIÓN

@tagHelperPrefix Especifica una cadena de prefijo de etiqueta para permitir la


compatibilidad con la aplicación auxiliar de etiquetas y hacer
explícito su uso.

Palabras clave reservadas de Razor


Palabras clave de Razor
page (requiere ASP.NET Core 2.0 y versiones posteriores)
namespace
funciones
hereda
modelo
section
helper (no admitida en ASP.NET Core actualmente)
Para hacer escape en una palabra clave de Razor, se usa @(Razor Keyword) (por ejemplo, @(functions) ).
Palabras clave C# de Razor
mayúsculas y minúsculas
do
default
for
foreach
if
else
bloquear
switch
try
catch
finally
utilizar
while
Las palabras clave C# de Razor deben tener doble escape con @(@C# Razor Keyword) (por ejemplo, @(@case) ). El
primer carácter @ hace escape en el analizador Razor y el segundo @ , en el analizador de C#.
Palabras clave reservadas no usadas en Razor
clase

Visualización de la clase C# de Razor generada por una vista


Agregue la siguiente clase al proyecto de ASP.NET Core MVC:
using Microsoft.AspNetCore.Mvc.Razor.Extensions;
using Microsoft.AspNetCore.Razor.Language;

public class CustomTemplateEngine : MvcRazorTemplateEngine


{
public CustomTemplateEngine(RazorEngine engine, RazorProject project)
: base(engine, project)
{
}

public override RazorCSharpDocument GenerateCode(RazorCodeDocument codeDocument)


{
var csharpDocument = base.GenerateCode(codeDocument);
var generatedCode = csharpDocument.GeneratedCode;

// Look at generatedCode

return csharpDocument;
}
}

Invalide el elemento RazorTemplateEngine agregado por MVC con la clase CustomTemplateEngine :

public void ConfigureServices(IServiceCollection services)


{
services.AddMvc();
services.AddSingleton<RazorTemplateEngine, CustomTemplateEngine>();
}

Establezca un punto de interrupción en la instrucción return csharpDocument de CustomTemplateEngine . Cuando


la ejecución del programa se detenga en el punto de interrupción, vea el valor de generatedCode .

Búsquedas de vistas y distinción entre mayúsculas y minúsculas


El motor de vista de Razor realiza búsquedas de vistas en las que se distingue entre mayúsculas y minúsculas.
Pero la búsqueda real viene determinada por el sistema de archivos subyacente:
Origen basado en archivos:
En los sistemas operativos con sistemas de archivos que no distinguen entre mayúsculas y minúsculas
(por ejemplo, Windows), las búsquedas de proveedor de archivos físicos no distinguirán mayúsculas
de minúsculas. Por ejemplo, return View("Test") arrojará como resultados /Views/Home/Test.cshtml,
/Views/home/test.cshtml y cualquier otra variante de mayúsculas y minúsculas.
En los sistemas de archivos en los que sí se distingue entre mayúsculas y minúsculas (por ejemplo,
Linux, OSX y al usar EmbeddedFileProvider ), las búsquedas distinguirán mayúsculas de minúsculas. Por
ejemplo, return View("Test") mostrará el resultado /Views/Home/Test.cshtml única y exclusivamente.
Vistas precompiladas: En ASP.NET Core 2.0 y versiones posteriores, las búsquedas de vistas precompiladas
no distinguen mayúsculas de minúsculas en todos los sistemas operativos. Este comportamiento es idéntico
al comportamiento del proveedor de archivos físicos en Windows. Si dos vistas precompiladas difieren solo
por sus mayúsculas o minúsculas, el resultado de la búsqueda será no determinante.
Por tanto, se anima a todos los desarrolladores a intentar que las mayúsculas y minúsculas de los nombres de
archivo y de directorio sean las mismas que las mayúsculas y minúsculas de:

* <span data-ttu-id="08942-293">Nombres de acciones, controladores y áreas.</span><span class="sxs-lookup">


<span data-stu-id="08942-293">Area, controller, and action names.</span></span>
* <span data-ttu-id="08942-294">Páginas de Razor.</span><span class="sxs-lookup"><span data-stu-id="08942-
294">Razor Pages.</span></span>

La coincidencia de mayúsculas y minúsculas garantiza que las implementaciones van a encontrar sus vistas,
independientemente de cuál sea el sistema de archivos subyacente.
Compilación de archivos de Razor en ASP.NET Core
25/06/2018 • 4 minutes to read • Edit Online

Por Rick Anderson


Un archivo de Razor se compila en tiempo de ejecución, cuando se invoca la vista MVC asociada. No se admite la
publicación de archivos de Razor en tiempo de compilación. Opcionalmente, los archivos de Razor se pueden
compilar en el momento de la publicación e implementar con la aplicación, mediante la herramienta de
precompilación.
Un archivo de Razor se compila en tiempo de ejecución, cuando se invoca la vista MVC o la página de Razor
asociada. No se admite la publicación de archivos de Razor en tiempo de compilación. Opcionalmente, los archivos
de Razor se pueden compilar en el momento de la publicación e implementar con la aplicación, mediante la
herramienta de precompilación.
Un archivo de Razor se compila en tiempo de ejecución, cuando se invoca la vista MVC o la página de Razor
asociada. Los archivos de Razor se compilan en tiempo de compilación y publicación mediante el SDK de Razor.

Consideraciones de precompilación
Los siguientes son los efectos secundarios de la precompilación de los archivos de Razor:
Un paquete publicado más pequeño.
Un menor tiempo de inicio.
Los archivos de Razor no se pueden editar; el contenido asociado no está presente en el paquete publicado.

Implementar archivos precompilados


La compilación de los archivos de Razor en tiempo de publicación y compilación está habilitada de manera
predeterminada en el SDK de Razor. La edición de los archivos de Razor después de que se actualicen se admite en
tiempo de compilación. De forma predeterminada, solo se implementa con la aplicación el archivo Views.dll
compilado y ningún archivo .cshtml.

IMPORTANT
El SDK de Razor solo es efectivo cuando no hay propiedades específicas de la precompilación establecidas en el archivo de
proyecto. Por ejemplo, establecer la propiedad MvcRazorCompileOnPublish del archivo .csproj en true deshabilita el SDK
de Razor.

Si el proyecto tiene como destino .NET Framework, instale el paquete NuGet


Microsoft.AspNetCore.Mvc.Razor.ViewCompilation:
[!code-xml]
Si el proyecto tiene como destino .NET Core, no es necesario hacer cambios.
Las plantillas de proyecto de ASP.NET Core 2.x establecen implícitamente la propiedad MvcRazorCompileOnPublish
en true de forma predeterminada. Por tanto, este elemento se puede quitar de manera segura del archivo .csproj.
IMPORTANT
La precompilación de los archivos de Razor no está disponible cuando se realiza una implementación independiente (SCD) en
ASP.NET Core 2.0.

Establezca la propiedad MvcRazorCompileOnPublish en true e instale el paquete NuGet


Microsoft.AspNetCore.Mvc.Razor.ViewCompilation. El siguiente ejemplo de .csproj resalta estas opciones:
[!code-xml]
Prepare la aplicación para una implementación independiente de la plataforma con el comando de publicación de
la CLI de .NET Core. Por ejemplo, ejecute el siguiente comando en la raíz del proyecto:

dotnet publish -c Release

Se crea un archivo <Nombre_proyecto>.PrecompiledViews.dll, que contiene los archivos de Razor compilados,


cuando la precompilación se realiza correctamente. Por ejemplo, en la captura de pantalla siguiente se muestra el
contenido de Index.cshtml en WebApplication1.PrecompiledViews.dll:

Recursos adicionales
Vistas de ASP.NET Core MVC
Introduction to Razor Pages in ASP.NET Core
Vistas de ASP.NET Core MVC
Introduction to Razor Pages in ASP.NET Core
Vistas de ASP.NET Core MVC
ASP.NET Core Razor SDK
Diseño en ASP.NET Core
25/06/2018 • 10 minutes to read • Edit Online

Por Steve Smith


Las vistas a menudo comparten elementos visuales y elementos mediante programación. En este
artículo aprenderá a usar diseños comunes, compartir directivas y ejecutar código común antes de
representar vistas en una aplicación ASP.NET.

Qué es un diseño
La mayoría de las aplicaciones web tienen un diseño común que ofrece al usuario una experiencia
coherente mientras navegan por sus páginas. El diseño suele incluir elementos comunes en la interfaz de
usuario, como el encabezado, los elementos de navegación o de menú y el pie de página de la aplicación.

Las estructuras HTML comunes, como los scripts y las hojas de estilos también se usan con frecuencia en
muchas páginas dentro de una aplicación. Todos estos elementos compartidos se pueden definir en un
archivo de diseño, al que se puede hacer referencia por cualquier vista que se use en la aplicación. Los
diseños reducen el código duplicado en las vistas y ayudan a seguir el principio Una vez y solo una
(DRY ).
Por convención, el diseño predeterminado para una aplicación ASP.NET se denomina _Layout.cshtml . La
plantilla de proyecto de Visual Studio ASP.NET Core MVC incluye este archivo de diseño en la carpeta
Views/Shared :
Este diseño define una plantilla de nivel superior para las vistas en la aplicación. Las aplicaciones no
necesitan un diseño y las aplicaciones pueden definir más de un diseño con distintas vistas que
especifiquen diseños diferentes.
Un ejemplo _Layout.cshtml :

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - WebApplication1</title>

<environment names="Development">
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
<link rel="stylesheet" href="~/css/site.css" />
</environment>
<environment names="Staging,Production">
<link rel="stylesheet"
href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.6/css/bootstrap.min.css"
asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-
test-value="absolute" />
<link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" />
</environment>
</head>
<body>
<div class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-
target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a asp-area="" asp-controller="Home" asp-action="Index" class="navbar-
brand">WebApplication1</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a asp-area="" asp-controller="Home" asp-action="Index">Home</a></li>
<li><a asp-area="" asp-controller="Home" asp-action="About">About</a></li>
<li><a asp-area="" asp-controller="Home" asp-action="Contact">Contact</a></li>
</ul>
@await Html.PartialAsync("_LoginPartial")
</div>
</div>
</div>
<div class="container body-content">
@RenderBody()
<hr />
<footer>
<p>&copy; 2016 - WebApplication1</p>
</footer>
</footer>
</div>

<environment names="Development">
<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
</environment>
<environment names="Staging,Production">
<script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-2.2.0.min.js"
asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
asp-fallback-test="window.jQuery">
</script>
<script src="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.6/bootstrap.min.js"
asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.min.js"
asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal">
</script>
<script src="~/js/site.min.js" asp-append-version="true"></script>
</environment>

@RenderSection("scripts", required: false)


</body>
</html>

Especificar un diseño
Las vistas de Razor tienen una propiedad Layout . Las vistas individuales especifican un diseño al
configurar esta propiedad:

@{
Layout = "_Layout";
}

El diseño especificado puede usar una ruta de acceso completa (ejemplo: /Views/Shared/_Layout.cshtml )
o un nombre parcial (ejemplo: _Layout ). Cuando se proporciona un nombre parcial, el motor de vista de
Razor buscará el archivo de diseño mediante su proceso de detección estándar. Primero se busca en la
carpeta asociada al controlador y después en la carpeta Shared . Este proceso de detección es idéntico al
usado para detectar vistas parciales.
De forma predeterminada, todos los diseños deben llamar a RenderBody . Cada vez que se realiza la
llamada a RenderBody , se representa el contenido de la vista.
Secciones
Opcionalmente, un diseño puede hacer referencia a una o varias secciones mediante una llamada a
RenderSection . Las secciones permiten organizar dónde se deben colocar determinados elementos de la
página. Cada llamada a RenderSection puede especificar si esa sección es obligatoria u opcional. Si no se
encuentra una sección obligatoria, se producirá una excepción. Las vistas individuales especifican el
contenido que se va a representar dentro de una sección con la sintaxis @section de Razor. Si una vista
define una sección, se debe representar (o se producirá un error).
Ejemplo de definición de @section en una vista:

@section Scripts {
<script type="text/javascript" src="/scripts/main.js"></script>
}

En el código anterior, se agregan scripts de validación a la sección scripts en una vista que incluye un
formulario. Es posible que otras vistas de la misma aplicación no necesiten scripts adicionales, por lo que
no sería necesario definir una sección de scripts.
Las secciones definidas en una vista solo están disponibles en su página de diseño inmediato. No se
puede hacer referencia a ellas desde líneas de código parcialmente ejecutadas, componentes de vista u
otros elementos del sistema de vistas.
Omitir secciones
De forma predeterminada, el cuerpo y todas las secciones de una página de contenido deben
representarse mediante la página de diseño. Para cumplir con esto, el motor de vistas de Razor
comprueba si el cuerpo y cada sección se han representado.
Para indicar al motor de vistas que pase por alto el cuerpo o las secciones, llame a los métodos
IgnoreBody y IgnoreSection .

El cuerpo y todas las secciones de una página de Razor deben representarse o pasarse por alto.

Importar directivas compartidas


Las vistas pueden usar directivas de Razor para hacer muchas cosas, como importar espacios de
nombres o realizar una inserción de dependencias. Se pueden especificar varias directivas compartidas
por muchas vistas en un archivo _ViewImports.cshtml común. El archivo _ViewImports es compatible
con estas directivas:
@addTagHelper

@removeTagHelper

@tagHelperPrefix

@using

@model

@inherits

@inject

El archivo no es compatible con otras características de Razor, como las funciones y las definiciones de
sección.
Archivo _ViewImports.cshtml de ejemplo:

@using WebApplication1
@using WebApplication1.Models
@using WebApplication1.Models.AccountViewModels
@using WebApplication1.Models.ManageViewModels
@using Microsoft.AspNetCore.Identity
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

El archivo _ViewImports.cshtml para una aplicación ASP.NET Core MVC normalmente se coloca en la
carpeta Views . Un archivo _ViewImports.cshtml puede colocarse dentro de cualquier carpeta, en cuyo
caso solo se aplicará a vistas dentro de esa carpeta y sus subcarpetas. Los archivos _ViewImports se
procesan a partir del nivel de raíz y después para cada carpeta que lleva hasta la ubicación de la propia
vista, por lo que la configuración especificada en el nivel de raíz es posible que se invalide en el nivel de
carpeta.
Por ejemplo, si un archivo _ViewImports.cshtml de nivel de raíz especifica @model y @addTagHelper , y
otro archivo _ViewImports.cshtml en la carpeta asociada al controlador de la vista especifica un @model
diferente y agrega otro @addTagHelper , la vista tendrá acceso a las aplicaciones auxiliares de etiquetas y
usará el último @model .
Si se ejecutan varios archivos _ViewImports.cshtml para una vista, el comportamiento combinado de las
directivas incluidas en los archivos ViewImports.cshtml será el siguiente:
@addTagHelper , @removeTagHelper : todos se ejecutan en orden
@tagHelperPrefix : el más cercano a la vista invalida los demás
@model : el más cercano a la vista invalida los demás
@inherits : el más cercano a la vista invalida los demás
@using : todos se incluyen y se omiten los duplicados
: para cada propiedad, la más cercana a la vista invalida cualquier otra con el mismo
@inject
nombre de propiedad

Ejecutar código antes de cada vista


Si tiene código que debe ejecutar antes de cada vista, debe colocarlo en el archivo _ViewStart.cshtml .
Por convención, el archivo _ViewStart.cshtml se encuentra en la carpeta Views . Las instrucciones que
aparecen en _ViewStart.cshtml se ejecutan antes de cada vista completa (no los diseños ni las vistas
parciales). Al igual que ViewImports.cshtml, _ViewStart.cshtml tiene una estructura jerárquica. Si se
define un archivo _ViewStart.cshtml en la carpeta de vista asociada al controlador, se ejecutará después
del que está definido en la raíz de la carpeta Views (si existe).
Archivo _ViewStart.cshtml de ejemplo:

@{
Layout = "_Layout";
}

El archivo anterior especifica que todas las vistas usarán el diseño _Layout.cshtml .

NOTE
Ni _ViewStart.cshtml ni _ViewImports.cshtml se colocan normalmente en la carpeta /Views/Shared . Las
versiones de nivel de aplicación de estos archivos deben colocarse directamente en la carpeta /Views .
Aplicaciones auxiliares de etiquetas en
ASP.NET Core
14/05/2018 • 25 minutes to read • Edit Online

Por Rick Anderson

¿Qué son las aplicaciones auxiliares de etiquetas?


Las aplicaciones auxiliares de etiquetas permiten que el código de servidor participe en la
creación y la representación de elementos HTML en archivos de Razor. Por ejemplo, la
aplicación auxiliar ImageTagHelper integrada puede anexar un número de versión al nombre
de imagen. Cada vez que la imagen cambia, el servidor genera una nueva versión única
para la imagen, lo que garantiza que los clientes puedan obtener la imagen actual (en lugar
de una imagen obsoleta almacenada en caché). Hay muchas aplicaciones auxiliares de
etiquetas integradas para tareas comunes (como la creación de formularios, vínculos, carga
de activos, etc.) y existen muchas más a disposición en repositorios públicos de GitHub y
como paquetes NuGet. Las aplicaciones auxiliares de etiquetas se crean en C# y tienen
como destino elementos HTML en función del nombre de elemento, el nombre de atributo
o la etiqueta principal. Por ejemplo, la aplicación auxiliar LabelTagHelper integrada puede
tener como destino el elemento HTML <label> cuando se aplican atributos
LabelTagHelper . Si está familiarizado con las aplicaciones auxiliares HTML, las aplicaciones
auxiliares de etiquetas reducen las transiciones explícitas entre HTML y C# en las vistas de
Razor. En muchos casos, las aplicaciones auxiliares HTML proporcionan un método
alternativo para una aplicación auxiliar de etiquetas específica, pero es importante tener en
cuenta que las aplicaciones auxiliares de etiquetas no reemplazan a las aplicaciones
auxiliares HTML y que no hay una aplicación auxiliar de etiquetas para cada aplicación
auxiliar HTML. En Comparación entre las aplicaciones auxiliares de etiquetas y las
aplicaciones auxiliares HTML se explican las diferencias con más detalle.

¿Qué proporcionan las aplicaciones auxiliares de


etiquetas?
Una experiencia de desarrollo compatible con HTML La mayor parte del marcado de
Razor con aplicaciones auxiliares de etiquetas es similar a HTML estándar. Los diseñadores
de front-end familiarizados con HTML, CSS y JavaScript pueden editar Razor sin tener que
aprender la sintaxis Razor de C#.
Un entorno de IntelliSense enriquecido para crear marcado HTML y Razor Este es
un marcado contraste con las aplicaciones auxiliares HTML, el método anterior para la
creación en el lado servidor de marcado en vistas de Razor. En Comparación entre las
aplicaciones auxiliares de etiquetas y las aplicaciones auxiliares HTML se explican las
diferencias con más detalle. En Compatibilidad de IntelliSense con aplicaciones auxiliares de
etiquetas se explica el entorno de IntelliSense. Incluso los desarrolladores que tienen
experiencia con la sintaxis Razor de C# son más productivos cuando usan aplicaciones
auxiliares de etiquetas que al escribir marcado de Razor de C#.
Una forma de ser más productivo y generar código más sólido, confiable y fácil de
mantener con información que solo está disponible en el servidor Por ejemplo, lo
habitual a la hora de actualizar las imágenes era cambiar el nombre de la imagen cuando se
modificaba. Las imágenes debían almacenarse en caché de forma activa por motivos de
rendimiento y, a menos que se cambiase el nombre de una imagen, se corría el riesgo de
que los clientes obtuviesen una copia obsoleta. Antes, después de editar una imagen, era
necesario cambiarle el nombre y actualizar todas las referencias a la imagen en la aplicación
web. Esto no solo exigía mucho trabajo, sino que era propenso a errores (por ejemplo,
omitir una referencia, incluir accidentalmente una cadena incorrecta, etc.). La aplicación
auxiliar ImageTagHelper integrada puede hacerlo automáticamente. ImageTagHelper puede
anexar un número de versión al nombre de la imagen, por lo que cada vez que la imagen
cambia, el servidor genera automáticamente una nueva versión única de la imagen. Esto
garantiza que los clientes obtengan la imagen actual. Esta solidez y ahorro de trabajo se
consiguen de forma gratuita mediante el uso de ImageTagHelper .
La mayoría de las aplicaciones auxiliares de etiquetas integradas tienen como destino
elementos HTML estándar y proporcionan atributos del lado servidor del elemento. Por
ejemplo, el elemento <input> que muchas vistas usan en la carpeta Views/Account
contiene el atributo asp-for . Este atributo extrae el nombre de la propiedad de modelo
especificada en el HTML representado. Pensemos en una vista de Razor con el siguiente
modelo:

public class Movie


{
public int ID { get; set; }
public string Title { get; set; }
public DateTime ReleaseDate { get; set; }
public string Genre { get; set; }
public decimal Price { get; set; }
}

El siguiente marcado de Razor:

<label asp-for="Movie.Title"></label>

Se genera el siguiente código HTML:

<label for="Movie_Title">Title</label>

El atributo asp-for está disponible por medio de la propiedad For en LabelTagHelper.


Vea Creación de aplicaciones auxiliares de etiquetas para más información.

Administración del ámbito de las aplicaciones auxiliares


de etiquetas
El ámbito de las aplicaciones auxiliares de etiquetas se controla mediante una combinación
de @addTagHelper , @removeTagHelper y el carácter de exclusión "!".
@addTagHelper hace que las aplicaciones auxiliares de etiquetas estén disponibles
Si crea una aplicación web ASP.NET Core denominada AuthoringTagHelpers (sin
autenticación), el siguiente archivo Views/_ViewImports.cshtml se agregará al proyecto:

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, AuthoringTagHelpers
La directiva @addTagHelper hace que las aplicaciones auxiliares de etiquetas estén
disponibles en la vista. En este caso, el archivo de vista es Views/_ViewImports.cshtml, el
cual heredan de forma predeterminada todos los archivos de vista de la carpeta Views y los
subdirectorios, lo que hace que las aplicaciones auxiliares de etiquetas estén disponibles. El
código anterior usa la sintaxis de comodines ("*") para especificar que todas las aplicaciones
auxiliares de etiquetas del ensamblado especificado
(Microsoft.AspNetCore.Mvc.TagHelpers) estarán disponibles para todos los archivos de
vista del directorio o subdirectorio Views. El primer parámetro después de @addTagHelper
especifica las aplicaciones auxiliares de etiquetas que se van a cargar (usamos "*" para todas
las aplicaciones auxiliares de etiquetas), y el segundo parámetro
("Microsoft.AspNetCore.Mvc.TagHelpers") especifica el ensamblado que contiene las
aplicaciones auxiliares de etiquetas. Microsoft.AspNetCore.Mvc.TagHelpers es el
ensamblado para las aplicaciones auxiliares de etiquetas integradas de ASP.NET Core.
Para exponer todas las aplicaciones auxiliares de etiquetas de este proyecto (que crea un
ensamblado denominado AuthoringTagHelpers), use lo siguiente:

@using AuthoringTagHelpers
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, AuthoringTagHelpers

Si el proyecto contiene una aplicación auxiliar EmailTagHelper con el espacio de nombres


predeterminado ( AuthoringTagHelpers.TagHelpers.EmailTagHelper ), puede proporcionar el
nombre completo (FQN ) de la aplicación auxiliar de etiquetas:

@using AuthoringTagHelpers
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper AuthoringTagHelpers.TagHelpers.EmailTagHelper, AuthoringTagHelpers

Para agregar una aplicación auxiliar de etiquetas a una vista con un FQN, agregue primero
el FQN ( AuthoringTagHelpers.TagHelpers.EmailTagHelper ) y, después, el nombre del
ensamblado (AuthoringTagHelpers). La mayoría de los desarrolladores prefiere usar la
sintaxis de comodines "*". La sintaxis de comodines permite insertar el carácter comodín "*"
como sufijo en un FQN. Por ejemplo, cualquiera de las siguientes directivas incorporará
EmailTagHelper :

@addTagHelper AuthoringTagHelpers.TagHelpers.E*, AuthoringTagHelpers


@addTagHelper AuthoringTagHelpers.TagHelpers.Email*, AuthoringTagHelpers

Como se ha mencionado anteriormente, al agregar la directiva @addTagHelper al archivo


Views/_ViewImports.cshtml la aplicación auxiliar de etiquetas se pone a disposición de
todos los archivos de vista del directorio Views y los subdirectorios. Puede usar la directiva
@addTagHelper en archivos de vista específicos si quiere exponer la aplicación auxiliar de
etiquetas solo a esas vistas.
@removeTagHelper quita las aplicaciones auxiliares de etiquetas
@removeTagHelper tiene los mismos parámetros que @addTagHelper , y quita una aplicación
auxiliar de etiquetas que se ha agregado anteriormente. Por ejemplo, si se aplica
@removeTagHelper a una vista específica, se quita de la vista la aplicación auxiliar de
etiquetas especificada. Si se usa @removeTagHelper en un archivo
Views/Folder/_ViewImports.cshtml, se quita la aplicación auxiliar de etiquetas especificada
de todas las vistas de Folder.
Controlar el ámbito de la aplicación auxiliar de etiquetas con el archivo
_ViewImports.cshtml
Si agrega un archivo _ViewImports.cshtml a cualquier carpeta de vistas, el motor de vistas
aplica las directivas de ese archivo y del archivo Views/_ViewImports.cshtml. Si agregara un
archivo Views/Home/_ViewImports.cshtml vacío para las vistas de Home, no se produciría
ningún cambio porque el archivo _ViewImports.cshtml se suma. Todas las directivas
@addTagHelper que agregue al archivo Views/Home/_ViewImports.cshtml (que no estén en
el archivo predeterminado Views/_ViewImports.cshtml) expondrán esas aplicaciones
auxiliares de etiquetas únicamente a las vistas de la carpeta Home.
Excluir elementos individuales
Puede deshabilitar una aplicación auxiliar de etiquetas en el nivel de elemento con el
carácter de exclusión ("!") de la aplicación auxiliar de etiquetas. Por ejemplo, la validación de
Email se deshabilita en <span> con el carácter de exclusión de la aplicación auxiliar de
etiquetas:

<!span asp-validation-for="Email" class="text-danger"></!span>

Debe aplicar el carácter de exclusión de la aplicación auxiliar de etiquetas en la etiqueta de


apertura y de cierre. (El editor de Visual Studio agrega automáticamente el carácter de
exclusión a la etiqueta de cierre cuando se agrega uno a la etiqueta de apertura). Después
de agregar el carácter de exclusión, el elemento y los atributos de la aplicación auxiliar de
etiquetas ya no se muestran en una fuente distinta.
Usar @tagHelperPrefix para hacer explícito el uso de la aplicación auxiliar de etiquetas
La directiva @tagHelperPrefix permite especificar una cadena de prefijo de etiqueta para
habilitar la compatibilidad con la aplicación auxiliar de etiquetas y hacer explícito su uso. Por
ejemplo, podría agregar el marcado siguiente al archivo Views/_ViewImports.cshtml:

@tagHelperPrefix th:

En la imagen de código siguiente, el prefijo de la aplicación auxiliar de etiquetas se establece


en th: , por lo que solo los elementos con el prefijo th: admiten aplicaciones auxiliares de
etiquetas (los elementos habilitados para aplicaciones auxiliares de etiquetas tienen una
fuente distinta). Los elementos <label> y <input> tienen el prefijo de las aplicaciones
auxiliares de etiquetas y están habilitados para estas, a diferencia del elemento <span> .

Las mismas reglas de jerarquía que se aplican a @addTagHelper también se aplican a


@tagHelperPrefix .

Compatibilidad de IntelliSense con aplicaciones auxiliares


de etiquetas
Cuando se crea una aplicación web ASP.NET en Visual Studio, se agrega el paquete NuGet
"Microsoft.AspNetCore.Razor.Tools". Este es el paquete que agrega las herramientas de las
aplicaciones auxiliares de etiquetas.
Considere la posibilidad de escribir un elemento HTML <label> . En cuanto escriba <l en
el editor de Visual Studio, IntelliSense mostrará elementos coincidentes:

No solo obtendrá ayuda HTML, sino el icono (el "@" symbol with "<>" situado debajo).

Esto identifica el elemento como el destino de las aplicaciones auxiliares de etiquetas. Los
elementos HTML puros (como fieldset ) muestran el icono "<>".
Una etiqueta HTML pura <label> muestra la etiqueta HTML (con el tema de color de
Visual Studio predeterminado) en una fuente marrón, con los atributos en rojo y los valores
de atributo en azul.

Después de escribir <label , IntelliSense muestra los atributos HTML/CSS disponibles y


los atributos destinados a la aplicación auxiliar de etiquetas:

La finalización de instrucciones de IntelliSense permite presionar la tecla TAB para


completar la instrucción con el valor seleccionado:

En cuanto se especifica un atributo de la aplicación auxiliar de etiquetas, las fuentes de las


etiquetas y los atributos cambian. Si usa el tema de color predeterminado "Azul" o "Claro"
de Visual Studio, la fuente es púrpura en negrita. Si usa el tema "Oscuro", la fuente es verde
azulado en negrita. Las imágenes de este documento se han realizado con el tema
predeterminado.

Puede usar el acceso directo CompleteWord de Visual Studio (el valor predeterminado es
Ctrl+barra espaciadora) entre comillas dobles (""). Ahora se encuentra en C#, como si
estuviera en una clase de C#. IntelliSense muestra todos los métodos y propiedades en el
modelo de páginas. Los métodos y las propiedades están disponibles porque el tipo de
propiedad es ModelExpression . En la imagen siguiente se edita la vista Register , por lo que
RegisterViewModel está disponible.

IntelliSense muestra las propiedades y los métodos disponibles para el modelo en la


página. El entorno enriquecido de IntelliSense le ayuda a seleccionar la clase CSS:

Comparación entre las aplicaciones auxiliares de


etiquetas y las aplicaciones auxiliares HTML
Las aplicaciones auxiliares de etiquetas se asocian a elementos HTML en las vistas de Razor,
mientras que las aplicaciones auxiliares HTML se invocan como métodos intercalados con
HTML en las vistas de Razor. Observe el siguiente marcado de Razor, que crea una etiqueta
HTML con la clase CSS "caption":

@Html.Label("FirstName", "First Name:", new {@class="caption"})

El símbolo de arroba ( @ ) le indica a Razor que este es el inicio del código. Los dos
parámetros siguientes ("FirstName" y "First Name:") son cadenas, por lo que IntelliSense no
sirve de ayuda. El último argumento:

new {@class="caption"}

Es un objeto anónimo que se usa para representar atributos. Dado que class es una palabra
reservada en C#, use el símbolo @ para forzar a C# a interpretar "@class=" como un
símbolo (nombre de propiedad). Para un diseñador de front-end (es decir, alguien
familiarizado con HTML, CSS, JavaScript y otras tecnologías de cliente, pero desconocedor
de C# y Razor), la mayor parte de la línea le resulta ajena. Es necesario crear toda la línea
sin ayuda de IntelliSense.
Si usa LabelTagHelper , se puede escribir el mismo marcado de la manera siguiente:

Con la versión de la aplicación auxiliar de etiquetas, en cuanto escriba <l en el editor de


Visual Studio, IntelliSense mostrará elementos coincidentes:

IntelliSense le ayuda a escribir toda la línea. LabelTagHelper también tiene como valor
predeterminado el establecimiento del contenido del valor de atributo asp-for
("FirstName") en "First Name"; es decir, convierte las propiedades con grafía Camel en una
oración formada por el nombre de la propiedad con un espacio donde aparece cada nueva
letra mayúscula. En el marcado siguiente:

Se genera:

<label class="caption" for="FirstName">First Name</label>

El contenido con grafía Camel convertido en grafía de oración no se usa si agrega


contenido a <label> . Por ejemplo:

Se genera:

<label class="caption" for="FirstName">Name First</label>

En la imagen de código siguiente se muestra la parte Form de la vista de Razor


Views/Account/Register.cshtml generada a partir de la plantilla heredada de MVC de
ASP.NET 4.5.x incluida con Visual Studio 2015.
En el editor de Visual Studio se muestra el código de C# con un fondo gris. Por ejemplo, la
aplicación auxiliar HTML AntiForgeryToken :

@Html.AntiForgeryToken()

Se muestra con un fondo gris. La mayor parte del marcado en la vista de registro es de C#.
Compárelo con el método equivalente con aplicaciones auxiliares de etiquetas:

El marcado es mucho más ordenado y más fácil de leer, modificar y mantener que en el
método de aplicaciones auxiliares HTML. El código de C# se reduce a la cantidad mínima
que el servidor necesita conocer. En el editor de Visual Studio se muestra el marcado de
destino de una aplicación auxiliar de etiquetas en una fuente distinta.
Observe el grupo Email:

<div class="form-group">
<label asp-for="Email" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Email" class="form-control" />
<span asp-validation-for="Email" class="text-danger"></span>
</div>
</div>

Cada uno de los atributos "asp-" tiene un valor de "Email", pero "Email" no es una cadena.
En este contexto, "Email" es la propiedad de expresión del modelo de C# para
RegisterViewModel .

El editor de Visual Studio le ayuda a escribir todo el marcado en el método de aplicación


auxiliar de etiquetas del formulario de registro, mientras que Visual Studio no proporciona
ninguna ayuda para la mayor parte del código en el método de aplicaciones auxiliares
HTML. En Compatibilidad de IntelliSense con aplicaciones auxiliares de etiquetas se explica
en detalle cómo trabajar con aplicaciones auxiliares de etiquetas en el editor de Visual
Studio.

Comparación entre las aplicaciones auxiliares de


etiquetas y los controles de servidor web
Las aplicaciones auxiliares de etiquetas no poseen el elemento al que están
asociadas; simplemente participan en la representación del elemento y el contenido.
Los controles de servidor web de ASP.NET se declaran y se invocan en una página.
Los controles de servidor web tienen un ciclo de vida no trivial que puede dificultar
el desarrollo y la depuración.
Los controles de servidor web permiten agregar funcionalidad a los elementos
Document Object Model (DOM ) de cliente mediante el uso de un control cliente. Las
aplicaciones auxiliares de etiquetas no tienen ningún DOM.
Los controles de servidor web incluyen la detección automática del explorador. Las
aplicaciones auxiliares de etiquetas no tienen conocimiento del explorador.
Varias aplicaciones auxiliares de etiquetas pueden actuar en el mismo elemento (vea
Evitar conflictos de aplicaciones auxiliares de etiquetas), mientras que normalmente
no se pueden crear controles de servidor web.
Las aplicaciones auxiliares de etiquetas pueden modificar la etiqueta y el contenido
de los elementos HTML que tienen como ámbito, pero no modifican directamente
ningún otro elemento de una página. Los controles de servidor web tienen un
ámbito menos específico y pueden realizar acciones que afectan a otras partes de la
página, lo que puede tener efectos secundarios imprevistos.
Los controles de servidor web usan convertidores de tipos para convertir cadenas en
objetos. Con las aplicaciones auxiliares de etiquetas, trabajará de forma nativa en C#,
por lo que no necesitará ninguna conversión de tipos.
Los controles de servidor web usan System.ComponentModel para implementar el
comportamiento de componentes y controles en tiempo de diseño y en tiempo de
ejecución. System.ComponentModel incluye las clases base y las interfaces para
implementar atributos y convertidores de tipos, enlazarlos con orígenes de datos y
generar licencias para los componentes. Compare esto con las aplicaciones auxiliares
de etiquetas, que normalmente se derivan de TagHelper , y la clase base TagHelper
solo expone dos métodos, Process y ProcessAsync .

Personalizar la fuente de elemento de aplicaciones


auxiliares de etiquetas
Puede personalizar la fuente y el color en Herramientas > Opciones > Entorno >
Fuentes y colores:

Recursos adicionales
Creación de aplicaciones auxiliares de etiquetas
Trabajar con formularios
TagHelperSamples en GitHub contiene ejemplos de aplicaciones auxiliares de etiquetas
para trabajar con Bootstrap.
Crear aplicaciones auxiliares de etiquetas en
ASP.NET Core
25/06/2018 • 28 minutes to read • Edit Online

Por Rick Anderson


Vea o descargue el código de ejemplo (cómo descargarlo)

Introducción a las aplicaciones auxiliares de etiquetas


En este tutorial se proporciona una introducción a la programación de aplicaciones auxiliares de etiquetas. En
Introducción a las aplicaciones auxiliares de etiquetas se describen las ventajas que proporcionan las
aplicaciones auxiliares de etiquetas.
Una aplicación auxiliar de etiquetas es una clase que implementa la interfaz ITagHelper . A pesar de ello, cuando
se crea una aplicación auxiliar de etiquetas, normalmente se deriva de TagHelper , lo que da acceso al método
Process .

1. Cree un proyecto de ASP.NET Core denominado AuthoringTagHelpers. No necesita autenticación para


este proyecto.
2. Cree una carpeta para almacenar las aplicaciones auxiliares de etiquetas denominada TagHelpers. La
carpeta TagHelpers no es necesaria, pero es una convención razonable. Ahora vamos a empezar a escribir
algunas aplicaciones auxiliares de etiquetas simples.

Aplicación auxiliar de etiquetas mínima


En esta sección, escribirá una aplicación auxiliar de etiquetas que actualice una etiqueta de correo electrónico.
Por ejemplo:

<email>Support</email>

El servidor usará nuestra aplicación auxiliar de etiquetas de correo electrónico para convertir ese marcado en lo
siguiente:

<a href="mailto:Support@contoso.com">Support@contoso.com</a>

Es decir, una etiqueta delimitadora lo convierte en un vínculo de correo electrónico. Tal vez le interese si está
escribiendo un motor de blogs y necesita que envíe correos electrónicos a contactos de marketing, soporte
técnico y de otro tipo, todos ellos en el mismo dominio.
1. Agregue la siguiente clase EmailTagHelper a la carpeta TagHelpers.
using Microsoft.AspNetCore.Razor.TagHelpers;
using System.Threading.Tasks;

namespace AuthoringTagHelpers.TagHelpers
{
public class EmailTagHelper : TagHelper
{
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.TagName = "a"; // Replaces <email> with <a> tag
}
}
}

Notas:
Las aplicaciones auxiliares de etiquetas usan una convención de nomenclatura que tiene como
destino los elementos de la clase raíz (menos la parte TagHelper del nombre de clase). En este
ejemplo, el nombre raíz de EmailTagHelper es email, por lo que el destino será la etiqueta
<email> . Esta convención de nomenclatura debería funcionar para la mayoría de las aplicaciones
auxiliares de etiquetas. Más adelante veremos cómo invalidarla.
La clase EmailTagHelper se deriva de TagHelper . La clase TagHelper proporciona métodos y
propiedades para escribir aplicaciones auxiliares de etiquetas.
El método Process invalidado controla lo que hace la aplicación auxiliar de etiquetas cuando se
ejecuta. La clase TagHelper también proporciona una versión asincrónica ( ProcessAsync ) con los
mismos parámetros.
El parámetro de contexto para Process (y ProcessAsync ) contiene información relacionada con la
ejecución de la etiqueta HTML actual.
El parámetro de salida para Process (y ProcessAsync ) contiene un elemento HTML con estado
que representa el origen original usado para generar una etiqueta y contenido HTML.
El nombre de nuestra clase tiene un sufijo TagHelper, que no es necesario, pero es una
convención recomendada. Podría declarar la clase de la manera siguiente:

public class Email : TagHelper

2. Para hacer que la clase EmailTagHelper esté disponible para todas nuestras vistas de Razor, agregue la
directiva addTagHelper al archivo Views/_ViewImports.cshtml: [!code-html]
El código anterior usa la sintaxis de comodines para especificar que todas las aplicaciones auxiliares de
etiquetas del ensamblado estarán disponibles. La primera cadena después de @addTagHelper especifica la
aplicación auxiliar de etiquetas que se va a cargar (use "*" para todas las aplicaciones auxiliares de
etiquetas), mientras que la segunda cadena "AuthoringTagHelpers" especifica el ensamblado en el que se
encuentra la aplicación auxiliar de etiquetas. Además, tenga en cuenta que la segunda línea incorpora las
aplicaciones auxiliares de etiquetas de ASP.NET Core MVC mediante la sintaxis de comodines (esas
aplicaciones auxiliares se tratan en el tema Introducción a las aplicaciones auxiliares de etiquetas). Es la
directiva @addTagHelper la que hace que la aplicación auxiliar de etiquetas esté disponible para la vista de
Razor. Como alternativa, puede proporcionar el nombre completo (FQN ) de una aplicación auxiliar de
etiquetas como se muestra a continuación:
@using AuthoringTagHelpers
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper AuthoringTagHelpers.TagHelpers.EmailTagHelper, AuthoringTagHelpers

Para agregar una aplicación auxiliar de etiquetas a una vista con un FQN, agregue primero el FQN (
AuthoringTagHelpers.TagHelpers.EmailTagHelper ) y, después, el nombre del ensamblado ( AuthoringTagHelpers).
La mayoría de los desarrolladores prefiere usar la sintaxis de comodines. En Introducción a las aplicaciones
auxiliares de etiquetas se describe en detalle la adición y eliminación de aplicaciones auxiliares de etiquetas, la
jerarquía y la sintaxis de comodines.
3. Actualice el marcado del archivo Views/Home/Contact.cshtml con estos cambios:

@{
ViewData["Title"] = "Contact";
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>

<address>
One Microsoft Way<br />
Redmond, WA 98052<br />
<abbr title="Phone">P:</abbr>
425.555.0100
</address>

<address>
<strong>Support:</strong><email>Support</email><br />
<strong>Marketing:</strong><email>Marketing</email>
</address>

4. Ejecute la aplicación y use su explorador favorito para ver el código fuente HTML, a fin de comprobar que
las etiquetas de correo electrónico se han reemplazado por un marcado delimitador (por ejemplo,
<a>Support</a> ). Support y Marketing se representan como vínculos, pero no tienen un atributo href
que los haga funcionales. Esto lo corregiremos en la sección siguiente.

SetAttribute y SetContent
En esta sección, actualizaremos EmailTagHelper para que cree una etiqueta delimitadora válida para correo
electrónico. Lo actualizaremos para que tome información de una vista de Razor (en forma de atributo mail-to )
y la use al generar el delimitador.
Actualice la clase EmailTagHelper con lo siguiente:
public class EmailTagHelper : TagHelper
{
private const string EmailDomain = "contoso.com";

// Can be passed via <email mail-to="..." />.


// Pascal case gets translated into lower-kebab-case.
public string MailTo { get; set; }

public override void Process(TagHelperContext context, TagHelperOutput output)


{
output.TagName = "a"; // Replaces <email> with <a> tag

var address = MailTo + "@" + EmailDomain;


output.Attributes.SetAttribute("href", "mailto:" + address);
output.Content.SetContent(address);
}
}

Notas:
Los nombres de clase y propiedad con grafía Pascal para las aplicaciones auxiliares de etiquetas se
convierten a su grafía kebab en minúsculas. Por tanto, para usar el atributo MailTo , usará su equivalente
<email mail-to="value"/> .

La última línea establece el contenido completado para nuestra aplicación auxiliar de etiquetas
mínimamente funcional.
La línea resaltada muestra la sintaxis para agregar atributos:

public override void Process(TagHelperContext context, TagHelperOutput output)


{
output.TagName = "a"; // Replaces <email> with <a> tag

var address = MailTo + "@" + EmailDomain;


output.Attributes.SetAttribute("href", "mailto:" + address);
output.Content.SetContent(address);
}

Este enfoque funciona para el atributo "href" siempre y cuando no exista actualmente en la colección de
atributos. También puede usar el método output.Attributes.Add para agregar un atributo de aplicación auxiliar
de etiquetas al final de la colección de atributos de etiqueta.
1. Actualice el marcado del archivo Views/Home/Contact.cshtml con estos cambios: [!code-html]
2. Ejecute la aplicación y compruebe que genera los vínculos correctos.

NOTE
Si escribe la etiqueta de correo electrónico como de autocierre ( <email mail-to="Rick" /> ), la salida final
también será de autocierre. Para habilitar la capacidad de escribir la etiqueta únicamente con una etiqueta de
apertura ( <email mail-to="Rick"> ) debe decorar la clase con lo siguiente:

[HtmlTargetElement("email", TagStructure = TagStructure.WithoutEndTag)]


public class EmailVoidTagHelper : TagHelper
{
private const string EmailDomain = "contoso.com";
// Code removed for brevity

Con una aplicación auxiliar de etiquetas de correo electrónico de autocierre, el resultado sería
<a href="mailto:Rick@contoso.com" /> . Las etiquetas delimitadoras de autocierre no son HTML válido,
por lo que no le interesa crear una, pero tal vez le convenga crear una aplicación auxiliar de etiquetas de
autocierre. Las aplicaciones auxiliares de etiquetas establecen el tipo de la propiedad TagMode después de
leer una etiqueta.
ProcessAsync
En esta sección, escribiremos una aplicación auxiliar de correo electrónico asincrónica.
1. Reemplace la clase EmailTagHelper por el siguiente código:

public class EmailTagHelper : TagHelper


{
private const string EmailDomain = "contoso.com";
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
output.TagName = "a"; // Replaces <email> with <a> tag
var content = await output.GetChildContentAsync();
var target = content.GetContent() + "@" + EmailDomain;
output.Attributes.SetAttribute("href", "mailto:" + target);
output.Content.SetContent(target);
}
}

Notas:
Esta versión usa el método ProcessAsync asincrónico. El método GetChildContentAsync
asincrónico devuelve un valor Task que contiene TagHelperContent .
Use el parámetro output para obtener el contenido del elemento HTML.
2. Realice el cambio siguiente en el archivo Views/Home/Contact.cshtml para que la aplicación auxiliar de
etiquetas pueda obtener el correo electrónico de destino.

@{
ViewData["Title"] = "Contact";
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>

<address>
One Microsoft Way<br />
Redmond, WA 98052<br />
<abbr title="Phone">P:</abbr>
425.555.0100
</address>

<address>
<strong>Support:</strong><email>Support</email><br />
<strong>Marketing:</strong><email>Marketing</email>
</address>

3. Ejecute la aplicación y compruebe que genera vínculos de correo electrónico válidos.


RemoveAll, PreContent.SetHtmlContent y PostContent.SetHtmlContent
1. Agregue la siguiente clase BoldTagHelper a la carpeta TagHelpers.
using Microsoft.AspNetCore.Razor.TagHelpers;

namespace AuthoringTagHelpers.TagHelpers
{
[HtmlTargetElement(Attributes = "bold")]
public class BoldTagHelper : TagHelper
{
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.Attributes.RemoveAll("bold");
output.PreContent.SetHtmlContent("<strong>");
output.PostContent.SetHtmlContent("</strong>");
}
}
}

Notas:
El atributo [HtmlTargetElement] pasa un parámetro de atributo que especifica que todos los
elementos HTML que contengan un atributo HTML denominado "bold" coincidirán, y se ejecutará
el método de invalidación Process de la clase. En nuestro ejemplo, el método Process quita el
atributo "bold" y rodea el marcado contenedor con <strong></strong> .
Dado que no le interesa reemplazar el contenido existente de la etiqueta, debe escribir la etiqueta
de apertura <strong> con el método PreContent.SetHtmlContent y la etiqueta de cierre </strong>
con el método PostContent.SetHtmlContent .
2. Modifique la vista About.cshtml para que contenga un valor de atributo bold . A continuación se muestra
el código completado.

@{
ViewData["Title"] = "About";
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>

<p bold>Use this area to provide additional information.</p>

<bold> Is this bold?</bold>

3. Ejecute la aplicación. Puede usar el explorador que prefiera para inspeccionar el origen y comprobar el
marcado.
El atributo [HtmlTargetElement] anterior solo tiene como destino el marcado HTML que proporciona el
nombre de atributo "bold". La aplicación auxiliar de etiquetas no ha modificado el elemento <bold> .
4. Convierta en comentario la línea de atributo [HtmlTargetElement] y de forma predeterminada tendrá
como destino las etiquetas <bold> , es decir, el marcado HTML con formato <bold> . Recuerde que la
convención de nomenclatura predeterminada hará coincidir el nombre de clase BoldTagHelper con las
etiquetas <bold> .
5. Ejecute la aplicación y compruebe que la aplicación auxiliar de etiquetas procesa la etiqueta <bold> .

El proceso de decorar una clase con varios atributos [HtmlTargetElement] tiene como resultado una operación
OR lógica de los destinos. Por ejemplo, si se usa el código siguiente, una etiqueta bold o un atributo bold
coincidirán.
[HtmlTargetElement("bold")]
[HtmlTargetElement(Attributes = "bold")]
public class BoldTagHelper : TagHelper
{
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.Attributes.RemoveAll("bold");
output.PreContent.SetHtmlContent("<strong>");
output.PostContent.SetHtmlContent("</strong>");
}
}

Cuando se agregan varios atributos a la misma instrucción, el tiempo de ejecución los trata como una operación
AND lógica. Por ejemplo, en el código siguiente, un elemento HTML debe denominarse "bold" con un atributo
denominado "bold" ( <bold bold /> ) para que coincida.

[HtmlTargetElement("bold", Attributes = "bold")]

También puede usar [HtmlTargetElement] para cambiar el nombre del elemento de destino. Por ejemplo, si
quiere que BoldTagHelper tenga como destino etiquetas <MyBold> , use el atributo siguiente:

[HtmlTargetElement("MyBold")]

Pasar un modelo a una aplicación auxiliar de etiquetas


1. Agregue una carpeta Models.
2. Agregue la clase WebsiteContext siguiente a la carpeta Models:

using System;

namespace AuthoringTagHelpers.Models
{
public class WebsiteContext
{
public Version Version { get; set; }
public int CopyrightYear { get; set; }
public bool Approved { get; set; }
public int TagsToShow { get; set; }
}
}

3. Agregue la siguiente clase WebsiteInformationTagHelper a la carpeta TagHelpers.


using System;
using AuthoringTagHelpers.Models;
using Microsoft.AspNetCore.Razor.TagHelpers;

namespace AuthoringTagHelpers.TagHelpers
{
public class WebsiteInformationTagHelper : TagHelper
{
public WebsiteContext Info { get; set; }

public override void Process(TagHelperContext context, TagHelperOutput output)


{
output.TagName = "section";
output.Content.SetHtmlContent(
$@"<ul><li><strong>Version:</strong> {Info.Version}</li>
<li><strong>Copyright Year:</strong> {Info.CopyrightYear}</li>
<li><strong>Approved:</strong> {Info.Approved}</li>
<li><strong>Number of tags to show:</strong> {Info.TagsToShow}</li></ul>");
output.TagMode = TagMode.StartTagAndEndTag;
}
}
}

Notas:
Como se ha indicado anteriormente, las aplicaciones auxiliares de etiquetas convierten las
propiedades y nombres de clase de C# con grafía Pascal para aplicaciones auxiliares de etiquetas
en grafía kebab en minúsculas. Por tanto, para usar WebsiteInformationTagHelper en Razor, deberá
escribir <website-information /> .
No está identificando de manera explícita el elemento de destino con el atributo
[HtmlTargetElement] , por lo que el destino será el valor predeterminado de website-information .
Si ha aplicado el atributo siguiente (tenga en cuenta que no tiene grafía kebab, pero coincide con el
nombre de clase):

[HtmlTargetElement("WebsiteInformation")]

La etiqueta con grafía kebab en minúsculas <website-information /> no coincidiría. Si quiere usar el
atributo [HtmlTargetElement] , debe usar la grafía kebab como se muestra a continuación:

[HtmlTargetElement("Website-Information")]

Los elementos que son de autocierre no tienen contenido. En este ejemplo, el marcado de Razor
usará una etiqueta de autocierre, pero la aplicación auxiliar de etiquetas creará un elemento
section (que no es de autocierre, y el contenido se escribirá dentro del elemento section ). Por
tanto, debe establecer TagMode en StartTagAndEndTag para escribir la salida. Como alternativa,
puede convertir en comentario la línea donde se establece TagMode y escribir marcado con una
etiqueta de cierre. (Más adelante en este tutorial se proporciona marcado de ejemplo).
El signo de dólar $ de la línea siguiente usa una cadena interpolada:

$@"<ul><li><strong>Version:</strong> {Info.Version}</li>

4. Agregue el marcado siguiente a la vista About.cshtml. En el marcado resaltado se muestra la información


del sitio web.
@using AuthoringTagHelpers.Models
@{
ViewData["Title"] = "About";
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>

<p bold>Use this area to provide additional information.</p>

<bold> Is this bold?</bold>

<h3> web site info </h3>


<website-information info="new WebsiteContext {
Version = new Version(1, 3),
CopyrightYear = 1638,
Approved = true,
TagsToShow = 131 }" />

NOTE
En el marcado de Razor que se muestra a continuación:

<website-information info="new WebsiteContext {


Version = new Version(1, 3),
CopyrightYear = 1638,
Approved = true,
TagsToShow = 131 }" />

Razor sabe que el atributo info es una clase, no una cadena, y usted quiere escribir código de C#. Todos los
atributos de aplicaciones auxiliares de etiquetas que no sean una cadena deben escribirse sin el carácter @ .

5. Ejecute la aplicación y vaya a la vista About para ver la información del sitio web.

NOTE
Puede usar el marcado siguiente con una etiqueta de cierre y quitar la línea con TagMode.StartTagAndEndTag de
la aplicación auxiliar de etiquetas:

<website-information info="new WebsiteContext {


Version = new Version(1, 3),
CopyrightYear = 1638,
Approved = true,
TagsToShow = 131 }" >
</website-information>

Aplicación auxiliar de etiquetas de condición


La aplicación auxiliar de etiquetas de condición representa la salida cuando se pasa un valor true.
1. Agregue la siguiente clase ConditionTagHelper a la carpeta TagHelpers.
using Microsoft.AspNetCore.Razor.TagHelpers;

namespace AuthoringTagHelpers.TagHelpers
{
[HtmlTargetElement(Attributes = nameof(Condition))]
public class ConditionTagHelper : TagHelper
{
public bool Condition { get; set; }

public override void Process(TagHelperContext context, TagHelperOutput output)


{
if (!Condition)
{
output.SuppressOutput();
}
}
}
}

2. Reemplace el contenido del archivo Views/Home/Index.cshtml por el marcado siguiente:

@using AuthoringTagHelpers.Models
@model WebsiteContext

@{
ViewData["Title"] = "Home Page";
}

<div>
<h3>Information about our website (outdated):</h3>
<website-information info=@Model />
<div condition="@Model.Approved">
<p>
This website has <strong surround="em"> @Model.Approved </strong> been approved yet.
Visit www.contoso.com for more information.
</p>
</div>
</div>

3. Reemplace el método Index del controlador Home por el código siguiente:

public IActionResult Index(bool approved = false)


{
return View(new WebsiteContext
{
Approved = approved,
CopyrightYear = 2015,
Version = new Version(1, 3, 3, 7),
TagsToShow = 20
});
}

4. Ejecute la aplicación y vaya a la página principal. El marcado del elemento condicional div no se
representará. Anexe la cadena de consulta ?approved=true a la dirección URL (por ejemplo,
http://localhost:1235/Home/Index?approved=true ). approved se establece en true y se muestra el marcado
condicional.
NOTE
Use el operador nameof para especificar el atributo de destino en lugar de especificar una cadena, como hizo con la
aplicación auxiliar de etiquetas bold:

[HtmlTargetElement(Attributes = nameof(Condition))]
// [HtmlTargetElement(Attributes = "condition")]
public class ConditionTagHelper : TagHelper
{
public bool Condition { get; set; }

public override void Process(TagHelperContext context, TagHelperOutput output)


{
if (!Condition)
{
output.SuppressOutput();
}
}
}

El operador nameof protegerá el código si en algún momento debe refactorizarse (tal vez interese cambiar el nombre a
RedCondition ).

Evitar conflictos de aplicaciones auxiliares de etiquetas


En esta sección, escribirá un par de aplicaciones auxiliares de etiquetas de vinculación automática. La primera
reemplazará el marcado que contiene una dirección URL que empieza con HTTP por una etiqueta delimitadora
HTML que contiene la misma dirección URL (y, por tanto, produce un vínculo a la dirección URL ). La segunda
hará lo mismo para una dirección URL que empieza con WWW.
Dado que estas dos aplicaciones auxiliares están estrechamente relacionadas y tal vez las refactorice en el
futuro, las guardaremos en el mismo archivo.
1. Agregue la siguiente clase AutoLinkerHttpTagHelper a la carpeta TagHelpers.

[HtmlTargetElement("p")]
public class AutoLinkerHttpTagHelper : TagHelper
{
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
var childContent = await output.GetChildContentAsync();
// Find Urls in the content and replace them with their anchor tag equivalent.
output.Content.SetHtmlContent(Regex.Replace(
childContent.GetContent(),
@"\b(?:https?://)(\S+)\b",
"<a target=\"_blank\" href=\"$0\">$0</a>")); // http link version}
}
}

NOTE
La clase AutoLinkerHttpTagHelper tiene como destino elementos p y usa Regex para crear el delimitador.

2. Agregue el marcado siguiente al final del archivo Views/Home/Contact.cshtml:


@{
ViewData["Title"] = "Contact";
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>

<address>
One Microsoft Way<br />
Redmond, WA 98052<br />
<abbr title="Phone">P:</abbr>
425.555.0100
</address>

<address>
<strong>Support:</strong><email>Support</email><br />
<strong>Marketing:</strong><email>Marketing</email>
</address>

<p>Visit us at http://docs.asp.net or at www.microsoft.com</p>

3. Ejecute la aplicación y compruebe que la aplicación auxiliar de etiquetas representa el delimitador


correctamente.
4. Actualice la clase AutoLinker para que incluya la aplicación auxiliar de etiquetas AutoLinkerWwwTagHelper
que convertirá el texto www en una etiqueta delimitadora que también contenga el texto www original. El
código actualizado aparece resaltado a continuación:

[HtmlTargetElement("p")]
public class AutoLinkerHttpTagHelper : TagHelper
{
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
var childContent = await output.GetChildContentAsync();
// Find Urls in the content and replace them with their anchor tag equivalent.
output.Content.SetHtmlContent(Regex.Replace(
childContent.GetContent(),
@"\b(?:https?://)(\S+)\b",
"<a target=\"_blank\" href=\"$0\">$0</a>")); // http link version}
}
}

[HtmlTargetElement("p")]
public class AutoLinkerWwwTagHelper : TagHelper
{
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
var childContent = await output.GetChildContentAsync();
// Find Urls in the content and replace them with their anchor tag equivalent.
output.Content.SetHtmlContent(Regex.Replace(
childContent.GetContent(),
@"\b(www\.)(\S+)\b",
"<a target=\"_blank\" href=\"http://$0\">$0</a>")); // www version
}
}
}

5. Ejecute la aplicación. Observe que el texto www se representa como un vínculo, a diferencia del texto
HTTP. Si coloca un punto de interrupción en ambas clases, verá que la clase de la aplicación auxiliar de
etiquetas HTTP se ejecuta primero. El problema es que la salida de la aplicación auxiliar de etiquetas se
almacena en caché y, cuando se ejecuta la aplicación auxiliar de etiquetas WWW, sobrescribe la salida
almacenada en caché desde la aplicación auxiliar de etiquetas HTTP. Más adelante en el tutorial veremos
cómo se controla el orden en el que se ejecutan las aplicaciones auxiliares de etiquetas. Corregiremos el
código con lo siguiente:

public class AutoLinkerHttpTagHelper : TagHelper


{
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
var childContent = output.Content.IsModified ? output.Content.GetContent() :
(await output.GetChildContentAsync()).GetContent();

// Find Urls in the content and replace them with their anchor tag equivalent.
output.Content.SetHtmlContent(Regex.Replace(
childContent,
@"\b(?:https?://)(\S+)\b",
"<a target=\"_blank\" href=\"$0\">$0</a>")); // http link version}
}
}

[HtmlTargetElement("p")]
public class AutoLinkerWwwTagHelper : TagHelper
{
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
var childContent = output.Content.IsModified ? output.Content.GetContent() :
(await output.GetChildContentAsync()).GetContent();

// Find Urls in the content and replace them with their anchor tag equivalent.
output.Content.SetHtmlContent(Regex.Replace(
childContent,
@"\b(www\.)(\S+)\b",
"<a target=\"_blank\" href=\"http://$0\">$0</a>")); // www version
}
}

NOTE
La primera vez que editó las aplicaciones auxiliares de etiquetas de vinculación automática, obtuvo el contenido del
destino con el código siguiente:

var childContent = await output.GetChildContentAsync();

Es decir, ha llamado a GetChildContentAsync mediante la salida TagHelperOutput pasada al método


ProcessAsync . Como ya se ha indicado, como la salida se almacena en caché, prevalece la última aplicación
auxiliar de etiquetas que se ejecuta. Para corregir el error, ha usado el código siguiente:

var childContent = output.Content.IsModified ? output.Content.GetContent() :


(await output.GetChildContentAsync()).GetContent();

El código anterior comprueba si se ha modificado el contenido y, en caso afirmativo, obtiene el contenido del búfer
de salida.

6. Ejecute la aplicación y compruebe que los dos vínculos funcionan según lo previsto. Aunque podría
parecer que nuestra aplicación auxiliar de etiquetas de vinculación automática es correcta y está
completa, tiene un pequeño problema. Si la aplicación auxiliar de etiquetas WWW se ejecuta en primer
lugar, los vínculos www no serán correctos. Actualice el código mediante la adición de la sobrecarga
Order para controlar el orden en el que se ejecuta la etiqueta. La propiedad Order determina el orden
de ejecución en relación con las demás aplicaciones auxiliares de etiquetas que tienen como destino el
mismo elemento. El valor de orden predeterminado es cero, y se ejecutan en primer lugar las instancias
con los valores más bajos.
public class AutoLinkerHttpTagHelper : TagHelper
{
// This filter must run before the AutoLinkerWwwTagHelper as it searches and replaces http and
// the AutoLinkerWwwTagHelper adds http to the markup.
public override int Order
{
get { return int.MinValue; }
}

El código anterior garantizará que la aplicación auxiliar de etiquetas HTTP se ejecute antes que la
aplicación auxiliar de etiquetas WWW. Cambie Order a MaxValue y compruebe que el marcado
generado para la etiqueta WWW es incorrecto.

Inspeccionar y recuperar contenido secundario


Las aplicaciones auxiliares de etiquetas proporcionan varias propiedades para recuperar contenido.
El resultado de GetChildContentAsync se pueden anexar a output.Content .
Puede inspeccionar el resultado de GetChildContentAsync con GetContent .
Si modifica output.Content , el cuerpo de TagHelper no se ejecutará ni representará a menos que llame a
GetChildContentAsync , como en nuestro ejemplo de vinculación automática:

public class AutoLinkerHttpTagHelper : TagHelper


{
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
var childContent = output.Content.IsModified ? output.Content.GetContent() :
(await output.GetChildContentAsync()).GetContent();

// Find Urls in the content and replace them with their anchor tag equivalent.
output.Content.SetHtmlContent(Regex.Replace(
childContent,
@"\b(?:https?://)(\S+)\b",
"<a target=\"_blank\" href=\"$0\">$0</a>")); // http link version}
}
}

Varias llamadas a GetChildContentAsync devuelven el mismo valor y no vuelven a ejecutar el cuerpo de


TagHelper a menos que se pase un parámetro false que indique que no se use el resultado almacenado en
caché.
Aplicaciones auxiliares de etiquetas en formularios de
ASP.NET Core
19/06/2018 • 29 minutes to read • Edit Online

Por Rick Anderson, Dave Paquette y Jerrie Pelser


En este documento se explica cómo trabajar con formularios y se detallan los elementos HTML que se usan
habitualmente en un formulario. El elemento HTML Form proporciona el mecanismo principal que las aplicaciones
web usan a la hora de devolver datos al servidor. La mayor parte de este documento se centra en describir las
aplicaciones auxiliares de etiquetas y cómo pueden servir para crear formularios HTML eficaces de manera
productiva. Se recomienda leer Introduction to Tag Helpers (Introducción a las aplicaciones auxiliares de etiquetas)
antes de este documento.
En muchos casos, las aplicaciones auxiliares HTML proporcionan un método alternativo a una aplicación auxiliar de
etiquetas específica, pero es importante tener en cuenta que las aplicaciones auxiliares de etiquetas no reemplazan
a las aplicaciones auxiliares HTML y que, de igual modo, no hay una aplicación auxiliar de etiqueta para cada
aplicación auxiliar HTML. Si existe una alternativa de aplicación auxiliar HTML, se mencionará aquí.

Aplicación auxiliar de etiquetas de formulario (Form)


La aplicación auxiliar de etiquetas Form hace lo siguiente:
Genera el valor de atributo action del elemento HTML <FORM> de una acción de controlador MVC o ruta
con nombre.
Genera un token comprobación de solicitudes oculto que impide que se falsifiquen solicitudes entre sitios
(cuando se usa con el atributo [ValidateAntiForgeryToken] en el método de acción HTTP Post).
Proporciona el atributo asp-route-<Parameter Name> , donde <Parameter Name> se agrega a los valores de
ruta. Los parámetros routeValues de Html.BeginForm y Html.BeginRouteForm proporcionan una
funcionalidad similar.
Tiene Html.BeginForm y Html.BeginRouteForm como alternativa de aplicación auxiliar HTML.
Ejemplo:

<form asp-controller="Demo" asp-action="Register" method="post">


<!-- Input and Submit elements -->
</form>

La aplicación auxiliar de etiquetas Form anterior genera el siguiente HTML:

<form method="post" action="/Demo/Register">


<!-- Input and Submit elements -->
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

El tiempo de ejecución MVC genera el valor de atributo action de los atributos de aplicación auxiliar de etiquetas
Form asp-controller y asp-action . La aplicación auxiliar de etiquetas Form genera también un token de
comprobación de solicitudes oculto que impide que se falsifiquen solicitudes entre sitios (cuando se usa con el
atributo [ValidateAntiForgeryToken] en el método de acción HTTP Post). Proteger un elemento HTML Form puro
de la falsificación de solicitudes entre sitios no es tarea fácil, y la aplicación auxiliar de etiquetas Form presta este
servicio.
Uso de una ruta con nombre
El atributo de aplicación auxiliar de etiquetas asp-route puede generar también el marcado del atributo HTML
action . Una aplicación con una ruta denominada register podría usar el siguiente marcado para la página de
registro:

<form asp-route="register" method="post">


<!-- Input and Submit elements -->
</form>

Muchas de las vistas de la carpeta Views/Account (que se genera cuando se crea una aplicación web con Cuentas
de usuario individuales) contienen el atributo asp-route-returnurl:

<form asp-controller="Account" asp-action="Login"


asp-route-returnurl="@ViewData["ReturnUrl"]"
method="post" class="form-horizontal" role="form">

NOTE
Con las plantillas integradas, returnUrl se rellena automáticamente solo cuando alguien intenta obtener acceso a un
recurso autorizado, pero no se ha autenticado o no tiene autorización. Si se intenta realizar un acceso no autorizado, el
middleware de seguridad redirige a la página de inicio de sesión con returnUrl configurado.

Aplicación auxiliar de etiquetas de entrada (Input)


La aplicación auxiliar de etiquetas Input enlaza un elemento HTML <input> a una expresión de modelo en la vista
de Razor.
Sintaxis:

<input asp-for="<Expression Name>" />

La aplicación auxiliar de etiquetas Input hace lo siguiente:


Genera los atributos HTML y name del nombre de expresión especificado en el atributo asp-for .
id
asp-for="Property1.Property2" es equivalente a m => m.Property1.Property2 . El nombre de una expresión es
lo que se usa para el valor de atributo asp-for . Vea la sección Nombres de expresión para obtener más
información.
Establece el valor de atributo HTML type según los atributos de tipo de modelo y de anotación de datos
aplicados a la propiedad de modelo.
No sobrescribirá el valor de atributo HTML type cuando se especifique uno.
Genera atributos de validación HTML5 a partir de los atributos de anotación de datos aplicados a las
propiedades de modelo.
Tiene características de aplicación auxiliar HTML que se superponen a Html.TextBoxFor y Html.EditorFor .
Vea la sección Alternativas de aplicación auxiliar HTML a la aplicación auxiliar de etiquetas Input
para obtener más información.
Permite establecer tipado fuerte. Si el nombre de la propiedad cambia y no se actualiza la aplicación auxiliar
de etiquetas, aparecerá un error similar al siguiente:

An error occurred during the compilation of a resource required to process


this request. Please review the following specific error details and modify
your source code appropriately.

Type expected
'RegisterViewModel' does not contain a definition for 'Email' and no
extension method 'Email' accepting a first argument of type 'RegisterViewModel'
could be found (are you missing a using directive or an assembly reference?)

La aplicación auxiliar de etiquetas Input establece el atributo HTML type en función del tipo .NET. En la siguiente
tabla se enumeran algunos tipos .NET habituales y el tipo HTML generado correspondiente (no incluimos aquí
todos los tipos .NET).

TIPO DE .NET TIPO DE ENTRADA

Bool type=”checkbox”

String type=”text”

DateTime type=”datetime”

Byte type=”number”

Valor int. type=”number”

Single, Double type=”number”

En la siguiente tabla se muestran algunos atributos de anotación de datos comunes que la aplicación auxiliar de
etiquetas Input asignará a tipos de entrada concretos (no incluimos aquí todos los atributo de validación):

ATRIBUTO TIPO DE ENTRADA

[EmailAddress] type=”email”

[Url] type=”url”

[HiddenInput] type=”hidden”

[Phone] type=”tel”

[DataType(DataType.Password)] type=”password”

[DataType(DataType.Date)] type=”date”

[DataType(DataType.Time)] type=”time”

Ejemplo:
using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }

[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
}

@model RegisterViewModel

<form asp-controller="Demo" asp-action="RegisterInput" method="post">


Email: <input asp-for="Email" /> <br />
Password: <input asp-for="Password" /><br />
<button type="submit">Register</button>
</form>

El código anterior genera el siguiente HTML:

<form method="post" action="/Demo/RegisterInput">


Email:
<input type="email" data-val="true"
data-val-email="The Email Address field is not a valid email address."
data-val-required="The Email Address field is required."
id="Email" name="Email" value="" /> <br>
Password:
<input type="password" data-val="true"
data-val-required="The Password field is required."
id="Password" name="Password" /><br>
<button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

Las anotaciones de datos que se aplican a las propiedades Email y Password generan metadatos en el modelo. La
aplicación auxiliar de etiquetas Input usa esos metadatos del modelo y genera atributos HTML5 data-val-* . Vea
Model Validation (Validación del modelo). Estos atributos describen los validadores que se van a adjuntar a los
campos de entrada, lo que proporciona HTML5 discreto y validación de jQuery. Los atributos discretos tienen el
formato data-val-rule="Error Message" , donde "rule" es el nombre de la regla de validación (como
data-val-required , data-val-email , data-val-maxlength , etc.). Si aparece un mensaje de error en el atributo, se
mostrará como el valor del atributo data-val-rule . También hay atributos con el formato
data-val-ruleName-argumentName="argumentValue" que aportan más información sobre la regla, por ejemplo,
data-val-maxlength-max="1024" .

Alternativas de aplicación auxiliar HTML a la aplicación auxiliar de etiquetas Input


Html.TextBox , Html.TextBoxFor , Html.Editor y Html.EditorFor tienen características que se superponen a la
aplicación auxiliar de etiquetas Input. La aplicación auxiliar de etiquetas Input establecerá automáticamente el
atributo type , cosa que no ocurrirá con Html.TextBox ni Html.TextBoxFor . Html.Editor y Html.EditorFor
controlan colecciones, objetos complejos y plantillas, pero la aplicación auxiliar de etiquetas Input no. La aplicación
auxiliar de etiquetas Input, Html.EditorFor y Html.TextBoxFor están fuertemente tipados (usan expresiones
lambda), pero Html.TextBox y Html.Editor no (usan nombres de expresión).
HtmlAttributes
@Html.Editor() y @Html.EditorFor() usan una entrada ViewDataDictionary especial denominada htmlAttributes
al ejecutar sus plantillas predeterminadas. Si lo desea, este comportamiento se puede enriquecer con parámetros
additionalViewData . En la clave "htmlAttributes" se distingue entre mayúsculas y minúsculas. La clave
"htmlAttributes" se controla de forma similar al objeto htmlAttributes pasado a aplicaciones auxiliares de etiqueta
Input como @Html.TextBox() .

@Html.EditorFor(model => model.YourProperty,


new { htmlAttributes = new { @class="myCssClass", style="Width:100px" } })

Nombres de expresión
El valor del atributo asp-for es una ModelExpression y la parte de la derecha de una expresión lambda. Por tanto,
asp-for="Property1" se convierte en m => m.Property1 en el código generado, motivo por el que no es necesario
incluir el prefijo Model . Puede usar el carácter "@" para iniciar una expresión insertada y moverla antes de `m.`:
</span><span class="sxs-lookup">You can use the "@" character to start an inline expression and move before
the m. :

@{
var joe = "Joe";
}
<input asp-for="@joe" />

Se genera el siguiente HTML:

<input type="text" id="joe" name="joe" value="Joe" />

Con las propiedades de colección, asp-for="CollectionProperty[23].Member" genera el mismo nombre que


asp-for="CollectionProperty[i].Member" si i tiene el valor 23 .
Cuando ASP.NET Core MVC calcula el valor de ModelExpression , inspecciona varios orígenes, ModelState incluido.
Fíjese en <input type="text" asp-for="@Name" /> . El atributo value calculado es el primer valor distinto de null de:
La entrada ModelState con la clave "Name".
El resultado de la expresión Model.Name .
Navegar a las propiedades secundarias
También se puede navegar a las propiedades secundarias a través de la ruta de acceso de propiedades del modelo
de vista. Pensemos en una clase de modelo más compleja que contiene una propiedad secundaria Address .

public class AddressViewModel


{
public string AddressLine1 { get; set; }
}
public class RegisterAddressViewModel
{
public string Email { get; set; }

[DataType(DataType.Password)]
public string Password { get; set; }

public AddressViewModel Address { get; set; }


}

En la vista, enlazamos a Address.AddressLine1 :

@model RegisterAddressViewModel

<form asp-controller="Demo" asp-action="RegisterAddress" method="post">


Email: <input asp-for="Email" /> <br />
Password: <input asp-for="Password" /><br />
Address: <input asp-for="Address.AddressLine1" /><br />
<button type="submit">Register</button>
</form>

Se genera el siguiente código HTML para Address.AddressLine1 :

<input type="text" id="Address_AddressLine1" name="Address.AddressLine1" value="" />

Colecciones y nombres de expresión


Como ejemplo, un modelo que contiene una matriz de Colors :

public class Person


{
public List<string> Colors { get; set; }

public int Age { get; set; }


}

El método de acción:

public IActionResult Edit(int id, int colorIndex)


{
ViewData["Index"] = colorIndex;
return View(GetPerson(id));
}

El siguiente código de Razor muestra cómo se tiene acceso a un elemento Color concreto:

@model Person
@{
var index = (int)ViewData["index"];
}

<form asp-controller="ToDo" asp-action="Edit" method="post">


@Html.EditorFor(m => m.Colors[index])
<label asp-for="Age"></label>
<input asp-for="Age" /><br />
<button type="submit">Post</button>
</form>
La plantilla Views/Shared/EditorTemplates/String.cshtml:

@model string

<label asp-for="@Model"></label>
<input asp-for="@Model" /> <br />

Ejemplo en el que se usa List<T> :

public class ToDoItem


{
public string Name { get; set; }

public bool IsDone { get; set; }


}

El siguiente código de Razor muestra cómo iterar por una colección:

@model List<ToDoItem>

<form asp-controller="ToDo" asp-action="Edit" method="post">


<table>
<tr> <th>Name</th> <th>Is Done</th> </tr>

@for (int i = 0; i < Model.Count; i++)


{
<tr>
@Html.EditorFor(model => model[i])
</tr>
}

</table>
<button type="submit">Save</button>
</form>

La plantilla Views/Shared/EditorTemplates/ToDoItem.cshtml:

@model ToDoItem

<td>
<label asp-for="@Model.Name"></label>
@Html.DisplayFor(model => model.Name)
</td>
<td>
<input asp-for="@Model.IsDone" />
</td>

@*
This template replaces the following Razor which evaluates the indexer three times.
<td>
<label asp-for="@Model[i].Name"></label>
@Html.DisplayFor(model => model[i].Name)
</td>
<td>
<input asp-for="@Model[i].IsDone" />
</td>
*@
NOTE
Use siempre for (y no foreach ) para iterar por una lista. Evaluar un indizador en una expresión de LINQ puede ser
costoso, con lo cual esa posibilidad hay que reducirla al mínimo.

NOTE
El código de ejemplo comentado anterior muestra cómo reemplazaríamos la expresión lambda por el operador @ para tener
acceso a cada elemento ToDoItem de la lista.

Aplicación auxiliar de etiquetas de área de texto (Textarea)


La aplicación auxiliar de etiquetas Textarea Tag Helper es similar a la aplicación auxiliar de etiquetas Input.
Genera los atributos id y name , y los atributos de validación de datos del modelo de un elemento
<textarea>.
Permite establecer tipado fuerte.
Alternativa de aplicación auxiliar HTML: Html.TextAreaFor .

Ejemplo:

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class DescriptionViewModel
{
[MinLength(5)]
[MaxLength(1024)]
public string Description { get; set; }
}
}

@model DescriptionViewModel

<form asp-controller="Demo" asp-action="RegisterTextArea" method="post">


<textarea asp-for="Description"></textarea>
<button type="submit">Test</button>
</form>

Se genera el siguiente código HTML:


<form method="post" action="/Demo/RegisterTextArea">
<textarea data-val="true"
data-val-maxlength="The field Description must be a string or array type with a maximum length of
&#x27;1024&#x27;."
data-val-maxlength-max="1024"
data-val-minlength="The field Description must be a string or array type with a minimum length of
&#x27;5&#x27;."
data-val-minlength-min="5"
id="Description" name="Description">
</textarea>
<button type="submit">Test</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

Aplicación auxiliar de etiquetas Label


Genera el título de la etiqueta y el atributo for en un elemento de un nombre de expresión.
Alternativa de aplicación auxiliar HTML: Html.LabelFor .

Label Tag Helper proporciona las siguientes ventajas con respecto a un elemento HTML Label puro:
Obtendrá automáticamente el valor de la etiqueta descriptiva del atributo Display . El nombre para mostrar
que se busca puede cambiar con el tiempo y la combinación del atributo Display , y la aplicación auxiliar de
etiquetas Label aplicará el elemento Display en cualquier lugar donde se use.
El código fuente contiene menos marcado.
Permite establecer tipado fuerte con la propiedad de modelo.
Ejemplo:

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class SimpleViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }
}
}

@model SimpleViewModel

<form asp-controller="Demo" asp-action="RegisterLabel" method="post">


<label asp-for="Email"></label>
<input asp-for="Email" /> <br />
</form>

Se genera el siguiente código HTML para el elemento <label> :

<label for="Email">Email Address</label>

La aplicación auxiliar de etiquetas Label genera el valor de atributo for de "Email", que es el identificador asociado
al elemento <input> . Las aplicaciones auxiliares de etiqueta generan elementos id y for coherentes para que se
puedan asociar correctamente. El título de este ejemplo proviene del atributo Display . Si el modelo no contuviera
un atributo Display , el título sería el nombre de propiedad de la expresión.

Aplicaciones auxiliares de etiquetas de validación


Hay dos aplicaciones auxiliares de etiquetas de validación: Validation Message Tag Helper (que muestra un mensaje
de validación relativo a una única propiedad del modelo) y Validation Summary Tag Helper (que muestra un
resumen de los errores de validación). Input Tag Helper agrega atributos de validación del lado cliente HTML5 a
los elementos de entrada en función de los atributos de anotación de datos de las clases del modelo. La validación
también se realiza en el lado servidor. La aplicación auxiliar de etiquetas de validación muestra estos mensajes de
error cuando se produce un error de validación.
Aplicación auxiliar de etiquetas de mensaje de validación
Agrega el atributo HTML5 data-valmsg-for="property" al elemento span, que adjunta los mensajes de error
de validación en el campo de entrada de la propiedad de modelo especificada. Cuando se produce un error
de validación en el lado cliente, jQuery muestra el mensaje de error en el elemento <span> .
La validación también tiene lugar en el lado servidor. Puede que JavaScript esté deshabilitado en los clientes,
mientras que hay algunas validaciones que solo se pueden realizar en el lado servidor.
Alternativa de aplicación auxiliar HTML: Html.ValidationMessageFor .

Validation Message Tag Helper se usa con el atributo asp-validation-for en un elemento HTML span.

<span asp-validation-for="Email"></span>

La aplicación auxiliar de etiquetas de mensaje de validación generará el siguiente código HTML:

<span class="field-validation-valid"
data-valmsg-for="Email"
data-valmsg-replace="true"></span>

Validation Message Tag Helper suele usarse por lo general después de una aplicación auxiliar de etiquetas Input
en la misma propiedad. Gracias a esto, se mostrarán todos los mensajes de error de validación cerca de la entrada
que produjo el error.

NOTE
Debe tener una vista con las referencias de script de JavaScript y de jQuery adecuadas para la validación del lado cliente. Para
más información, vea Introduction to model validation in ASP.NET Core MVC (Introducción a la validación de modelos en
ASP.NET Core MVC).

Cuando se produce un error de validación del lado servidor (por ejemplo, porque haya una validación del lado
servidor personalizada o porque la validación del lado cliente esté deshabilitada), MVC pone ese mensaje de error
como cuerpo del elemento <span> .

<span class="field-validation-error" data-valmsg-for="Email"


data-valmsg-replace="true">
The Email Address field is required.
</span>

Aplicación auxiliar de etiquetas de resumen de validación


Tiene como destino todos los elementos <div> con el atributo asp-validation-summary .
Alternativa de aplicación auxiliar HTML: @Html.ValidationSummary .

Validation Summary Tag Helper se usa para mostrar un resumen de los mensajes de validación. El valor de atributo
asp-validation-summary puede ser cualquiera de los siguientes:

ASP-VALIDATION-SUMMARY MENSAJES DE VALIDACIÓN QUE SE MUESTRAN

ValidationSummary.All Nivel de modelo y de propiedad

ValidationSummary.ModelOnly Modelo

ValidationSummary.None Ninguna

Ejemplo
En el siguiente ejemplo, el modelo de datos se complementa con atributos DataAnnotation , lo que genera mensajes
de error de validación sobre el elemento <input> . Cuando se produce un error de validación, la aplicación auxiliar
de etiquetas de validación muestra el mensaje de error:

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }

[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
}

@model RegisterViewModel

<form asp-controller="Demo" asp-action="RegisterValidation" method="post">


<div asp-validation-summary="ModelOnly"></div>
Email: <input asp-for="Email" /> <br />
<span asp-validation-for="Email"></span><br />
Password: <input asp-for="Password" /><br />
<span asp-validation-for="Password"></span><br />
<button type="submit">Register</button>
</form>

El código HTML generado (cuando el modelo es válido) es este:


<form action="/DemoReg/Register" method="post">
<div class="validation-summary-valid" data-valmsg-summary="true">
<ul><li style="display:none"></li></ul></div>
Email: <input name="Email" id="Email" type="email" value=""
data-val-required="The Email field is required."
data-val-email="The Email field is not a valid email address."
data-val="true"> <br>
<span class="field-validation-valid" data-valmsg-replace="true"
data-valmsg-for="Email"></span><br>
Password: <input name="Password" id="Password" type="password"
data-val-required="The Password field is required." data-val="true"><br>
<span class="field-validation-valid" data-valmsg-replace="true"
data-valmsg-for="Password"></span><br>
<button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

Aplicación auxiliar de etiquetas de selección (Select)


Genera el elemento select y el elemento asociado option de las propiedades del modelo.
Tiene Html.DropDownListFor y Html.ListBoxFor como alternativa de aplicación auxiliar HTML.
El elemento asp-for de Select Tag Helper especifica el nombre de la propiedad de modelo del elemento select,
mientras que asp-items especifica los elementos option. Por ejemplo:

<select asp-for="Country" asp-items="Model.Countries"></select>

Ejemplo:

using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace FormsTagHelper.ViewModels
{
public class CountryViewModel
{
public string Country { get; set; }

public List<SelectListItem> Countries { get; } = new List<SelectListItem>


{
new SelectListItem { Value = "MX", Text = "Mexico" },
new SelectListItem { Value = "CA", Text = "Canada" },
new SelectListItem { Value = "US", Text = "USA" },
};
}
}

El método Index inicializa CountryViewModel , establece el país seleccionado y lo pasa a la vista Index .

public IActionResult IndexOption(int id)


{
var model = new CountryViewModel();
model.Country = "CA";
return View(model);
}

El método HTTP POST Index muestra la selección:


[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Index(CountryViewModel model)
{
if (ModelState.IsValid)
{
var msg = model.Country + " selected";
return RedirectToAction("IndexSuccess", new { message = msg});
}

// If we got this far, something failed; redisplay form.


return View(model);
}

La vista Index :

@model CountryViewModel

<form asp-controller="Home" asp-action="Index" method="post">


<select asp-for="Country" asp-items="Model.Countries"></select>
<br /><button type="submit">Register</button>
</form>

Genera el siguiente código HTML (con la selección "CA"):

<form method="post" action="/">


<select id="Country" name="Country">
<option value="MX">Mexico</option>
<option selected="selected" value="CA">Canada</option>
<option value="US">USA</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

NOTE
No se recomienda usar ViewBag o ViewData con la aplicación auxiliar de etiquetas Select. Un modelo de vista es más eficaz
a la hora de proporcionar metadatos MVC y suele ser menos problemático.

El valor de atributo asp-for es un caso especial y no necesita un prefijo Model , mientras que los otros atributos de
aplicación auxiliar de etiquetas sí (como asp-items ).

<select asp-for="Country" asp-items="Model.Countries"></select>

Enlace con enum


A menudo, conviene usar <select> con una propiedad enum y generar los elementos SelectListItem a partir de
valores enum .
Ejemplo:
public class CountryEnumViewModel
{
public CountryEnum EnumCountry { get; set; }
}
}

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public enum CountryEnum
{
[Display(Name = "United Mexican States")]
Mexico,
[Display(Name = "United States of America")]
USA,
Canada,
France,
Germany,
Spain
}
}

El método GetEnumSelectList genera un objeto SelectList para una enumeración.

@model CountryEnumViewModel

<form asp-controller="Home" asp-action="IndexEnum" method="post">


<select asp-for="EnumCountry"
asp-items="Html.GetEnumSelectList<CountryEnum>()"> >
</select>
<br /><button type="submit">Register</button>
</form>

Puede complementar la lista de enumeradores con el atributo Display para obtener una interfaz de usuario más
completa:

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public enum CountryEnum
{
[Display(Name = "United Mexican States")]
Mexico,
[Display(Name = "United States of America")]
USA,
Canada,
France,
Germany,
Spain
}
}

Se genera el siguiente código HTML:


<form method="post" action="/Home/IndexEnum">
<select data-val="true" data-val-required="The EnumCountry field is required."
id="EnumCountry" name="EnumCountry">
<option value="0">United Mexican States</option>
<option value="1">United States of America</option>
<option value="2">Canada</option>
<option value="3">France</option>
<option value="4">Germany</option>
<option selected="selected" value="5">Spain</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

Agrupamiento de opciones
El elemento HTML <optgroup> se genera cuando el modelo de vista contiene uno o varios objetos
SelectListGroup .

CountryViewModelGroup agrupa los elementos SelectListItem en los grupos "North America" y "Europe":
public class CountryViewModelGroup
{
public CountryViewModelGroup()
{
var NorthAmericaGroup = new SelectListGroup { Name = "North America" };
var EuropeGroup = new SelectListGroup { Name = "Europe" };

Countries = new List<SelectListItem>


{
new SelectListItem
{
Value = "MEX",
Text = "Mexico",
Group = NorthAmericaGroup
},
new SelectListItem
{
Value = "CAN",
Text = "Canada",
Group = NorthAmericaGroup
},
new SelectListItem
{
Value = "US",
Text = "USA",
Group = NorthAmericaGroup
},
new SelectListItem
{
Value = "FR",
Text = "France",
Group = EuropeGroup
},
new SelectListItem
{
Value = "ES",
Text = "Spain",
Group = EuropeGroup
},
new SelectListItem
{
Value = "DE",
Text = "Germany",
Group = EuropeGroup
}
};
}

public string Country { get; set; }

public List<SelectListItem> Countries { get; }

Aquí mostramos los dos grupos:


El código HTML generado:

<form method="post" action="/Home/IndexGroup">


<select id="Country" name="Country">
<optgroup label="North America">
<option value="MEX">Mexico</option>
<option value="CAN">Canada</option>
<option value="US">USA</option>
</optgroup>
<optgroup label="Europe">
<option value="FR">France</option>
<option value="ES">Spain</option>
<option value="DE">Germany</option>
</optgroup>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

Selección múltiple
La aplicación auxiliar de etiquetas Select generará automáticamente el atributo multiple = "multiple" si la propiedad
especificada en el atributo asp-for es IEnumerable . Por ejemplo, si tenemos el siguiente modelo:

using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace FormsTagHelper.ViewModels
{
public class CountryViewModelIEnumerable
{
public IEnumerable<string> CountryCodes { get; set; }

public List<SelectListItem> Countries { get; } = new List<SelectListItem>


{
new SelectListItem { Value = "MX", Text = "Mexico" },
new SelectListItem { Value = "CA", Text = "Canada" },
new SelectListItem { Value = "US", Text = "USA" },
new SelectListItem { Value = "FR", Text = "France" },
new SelectListItem { Value = "ES", Text = "Spain" },
new SelectListItem { Value = "DE", Text = "Germany"}
};
}
}

Con la siguiente vista:


@model CountryViewModelIEnumerable

<form asp-controller="Home" asp-action="IndexMultiSelect" method="post">


<select asp-for="CountryCodes" asp-items="Model.Countries"></select>
<br /><button type="submit">Register</button>
</form>

Se genera el siguiente código HTML:

<form method="post" action="/Home/IndexMultiSelect">


<select id="CountryCodes"
multiple="multiple"
name="CountryCodes"><option value="MX">Mexico</option>
<option value="CA">Canada</option>
<option value="US">USA</option>
<option value="FR">France</option>
<option value="ES">Spain</option>
<option value="DE">Germany</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

Sin selección
Si ve que usa la opción "sin especificar" en varias páginas, puede crear una plantilla para no tener que repetir el
código HTML:

@model CountryViewModel

<form asp-controller="Home" asp-action="IndexEmpty" method="post">


@Html.EditorForModel()
<br /><button type="submit">Register</button>
</form>

La plantilla Views/Shared/EditorTemplates/CountryViewModel.cshtml:

@model CountryViewModel

<select asp-for="Country" asp-items="Model.Countries">


<option value="">--none--</option>
</select>

La posibilidad de agregar elementos HTML <option> no se limita exclusivamente a los casos en los que no se
seleccionada nada. Por ejemplo, el método de acción y vista siguientes generarán un código HTML similar al
código anterior:

public IActionResult IndexOption(int id)


{
var model = new CountryViewModel();
model.Country = "CA";
return View(model);
}
@model CountryViewModel

<form asp-controller="Home" asp-action="IndexEmpty" method="post">


<select asp-for="Country">
<option value="">&lt;none&gt;</option>
<option value="MX">Mexico</option>
<option value="CA">Canada</option>
<option value="US">USA</option>
</select>
<br /><button type="submit">Register</button>
</form>

Se seleccionará el elemento <option> correcto (que contenga el atributo selected="selected" ) en función del valor
real de Country .

<form method="post" action="/Home/IndexEmpty">


<select id="Country" name="Country">
<option value="">&lt;none&gt;</option>
<option value="MX">Mexico</option>
<option value="CA" selected="selected">Canada</option>
<option value="US">USA</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

Recursos adicionales
Aplicaciones auxiliares de etiquetas
HTML Form element (Elemento HTML Form)
Request Verification Token (Token de comprobación de solicitudes)
Enlace de modelos
Introduction to model validation in ASP.NET Core MVC (Introducción a la validación de modelos en ASP.NET
Core MVC )
IAttributeAdapter Interface (Interfaz IAttributeAdapter)
Fragmentos de código de este documento
Aplicaciones auxiliares de etiquetas integradas de
ASP.NET Core
21/06/2018 • 2 minutes to read • Edit Online

Por Peter Kellner


ASP.NET Core incluye varias aplicaciones auxiliares de etiquetas integradas para aumentar la productividad. En
esta sección se proporciona un resumen de las aplicaciones auxiliares de etiquetas integradas.

NOTE
Algunas aplicaciones auxiliares de etiquetas integradas no figuran en la sección, ya que se usan internamente en el motor de
vistas de Razor. Esto incluye una aplicación auxiliar de etiquetas para el carácter ~, que se expande hasta la ruta de acceso raíz
del sitio web.

Aplicaciones auxiliares de etiquetas integradas de ASP.NET Core


Aplicación auxiliar de etiquetas de delimitador
Aplicación auxiliar de etiquetas de caché
Aplicación auxiliar de etiquetas de caché distribuida
Aplicación auxiliar de etiquetas de entorno
Aplicación auxiliar de etiquetas de formulario
Aplicación auxiliar de etiquetas de imagen
Aplicación auxiliar de etiquetas de entrada
Aplicación auxiliar de etiquetas de elementos de etiqueta
Aplicación auxiliar de etiquetas parciales
Aplicación auxiliar de etiquetas de selección
Aplicación auxiliar de etiquetas de área de texto
Aplicación auxiliar de etiquetas de mensaje de validación
Aplicación auxiliar de etiquetas de resumen de validación

Recursos adicionales
Desarrollo del lado cliente
Aplicaciones auxiliares de etiquetas
Aplicación auxiliar de etiquetas delimitadoras en
ASP.NET Core
25/06/2018 • 11 minutes to read • Edit Online

De Peter Kellner y Scott Addie


Vea o descargue el código de ejemplo (cómo descargarlo)
La aplicación auxiliar de etiquetas delimitadoras mejora la etiqueta delimitadora de código HTML estándar (
<a ... ></a> ) agregando nuevos atributos. Por convención, los nombres de atributo tienen el prefijo asp- . El
valor de atributo href del elemento delimitador representado se determina mediante los valores de los
atributos asp- .
En los ejemplos de todo este documento se usa SpeakerController:

using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Linq;

public class SpeakerController : Controller


{
private List<Speaker> Speakers =
new List<Speaker>
{
new Speaker {SpeakerId = 10},
new Speaker {SpeakerId = 11},
new Speaker {SpeakerId = 12}
};

[Route("Speaker/{id:int}")]
public IActionResult Detail(int id) =>
View(Speakers.FirstOrDefault(a => a.SpeakerId == id));

[Route("/Speaker/Evaluations",
Name = "speakerevals")]
public IActionResult Evaluations() => View();

[Route("/Speaker/EvaluationsCurrent",
Name = "speakerevalscurrent")]
public IActionResult Evaluations(
int speakerId,
bool currentYear) => View();

public IActionResult Index() => View(Speakers);


}

public class Speaker


{
public int SpeakerId { get; set; }
}

A continuación se proporciona un inventario de los atributos asp- .

asp-controller
El atributo asp-controller asigna el controlador usado para generar la dirección URL. En el marcado siguiente
se especifican todos los altavoces:
<a asp-controller="Speaker"
asp-action="Index">All Speakers</a>

El código HTML generado:

<a href="/Speaker">All Speakers</a>

Si el atributo asp-controller está especificado y asp-action no lo está, el valor asp-action predeterminado


es la acción del controlador asociada a la vista que se está ejecutando. Si se omite asp-action en el marcado
anterior y se usa la aplicación auxiliar de etiquetas delimitadoras en la vista Index de HomeController (/Home),
el código HTML generado es el siguiente:

<a href="/Home">All Speakers</a>

asp-action
El valor del atributo asp-action representa el nombre de la acción del controlador incluido en el atributo href
generado. El siguiente marcado establece el valor del atributo href generado en la página "Speaker
Evaluations":

<a asp-controller="Speaker"
asp-action="Evaluations">Speaker Evaluations</a>

El código HTML generado:

<a href="/Speaker/Evaluations">Speaker Evaluations</a>

Si no hay ningún atributo asp-controller especificado, se usará el controlador predeterminado que llama a la
vista que se está ejecutando.
Si el valor del atributo asp-action es Index , no se anexa ninguna acción a la dirección URL, lo que da lugar a
la invocación de la acción Index predeterminada. La acción especificada (o su valor predeterminado), debe
existir en el controlador al que se hace referencia en asp-controller .

asp-route-{valor}
El atributo asp-route-{value} permite indicar un prefijo de ruta comodín. Cualquier valor que ocupe el
marcador de posición {value} se interpretará como un parámetro de ruta potencial. Si no se encuentra
ninguna ruta predeterminada, este prefijo de ruta se anexará al atributo href generado como valor y
parámetro de solicitud. En caso contrario, se sustituirá en la plantilla de ruta.
Observe la siguiente acción del controlador:
public IActionResult AnchorTagHelper(int id)
{
var speaker = new Speaker
{
SpeakerId = id
};

return View(speaker);
}

Con una plantilla de ruta predeterminada definida en Startup.Configure:

app.UseMvc(routes =>
{
// need route and attribute on controller: [Area("Blogs")]
routes.MapRoute(name: "areaRoute",
template: "{area:exists}/{controller=Home}/{action=Index}");

// default route for non-areas


routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});

La vista de MVC usa el modelo, proporcionado por la acción, como se indica a continuación:

@model Speaker
<!DOCTYPE html>
<html>
<body>
<a asp-controller="Speaker"
asp-action="Detail"
asp-route-id="@Model.SpeakerId">SpeakerId: @Model.SpeakerId</a>
</body>
</html>

Se hace coincidir el marcador de posición {id?} de la ruta predeterminada. El código HTML generado:

<a href="/Speaker/Detail/12">SpeakerId: 12</a>

Supongamos que el prefijo de ruta no forma parte de la plantilla de enrutamiento coincidente, al igual que con
la siguiente vista de MVC:

@model Speaker
<!DOCTYPE html>
<html>
<body>
<a asp-controller="Speaker"
asp-action="Detail"
asp-route-speakerid="@Model.SpeakerId">SpeakerId: @Model.SpeakerId</a>
<body>
</html>

Se genera el siguiente código HTML porque no se encontró speakerid en la ruta coincidente:

<a href="/Speaker/Detail?speakerid=12">SpeakerId: 12</a>


Si no se especifica asp-controller o asp-action , se sigue el mismo proceso predeterminado que en el
atributo asp-route .

asp-route
El atributo asp-route se usa para crear una dirección URL que se vincula directamente con una ruta con
nombre. Mediante atributos de enrutamiento puede asignarse un nombre a una ruta tal y como se muestra en
SpeakerController y usarse en su acción Evaluations :

[Route("/Speaker/Evaluations",
Name = "speakerevals")]
public IActionResult Evaluations() => View();

En el siguiente marcado, el atributo asp-route hace referencia a la ruta con nombre:

<a asp-route="speakerevals">Speaker Evaluations</a>

La aplicación auxiliar de etiquetas delimitadoras genera una ruta directamente a esa acción de controlador
mediante la dirección URL /Speaker/Evaluations. El código HTML generado:

<a href="/Speaker/Evaluations">Speaker Evaluations</a>

Si además de asp-route se especifica asp-controller o asp-action , la ruta generada puede no ser la


esperada. Para evitar un conflicto de ruta, no se debe usar asp-route con los atributos asp-controller y
asp-action .

asp-all-route-data
El atributo asp-all-route-data permite crear un diccionario de pares clave-valor. La clave es el nombre del
parámetro, mientras que el valor es el valor del parámetro.
En el ejemplo siguiente se inicializa un diccionario y se pasa a una vista de Razor. Los datos también se
podrían pasar con el modelo.

@{
var parms = new Dictionary<string, string>
{
{ "speakerId", "11" },
{ "currentYear", "true" }
};
}

<a asp-route="speakerevalscurrent"
asp-all-route-data="parms">Speaker Evaluations</a>

El código anterior genera el siguiente código HTML:

<a href="/Speaker/EvaluationsCurrent?speakerId=11&currentYear=true">Speaker Evaluations</a>

Se acopla el diccionario asp-all-route-data para generar una cadena de consulta que cumpla los requisitos
de la acción Evaluations sobrecargada:
[Route("/Speaker/EvaluationsCurrent",
Name = "speakerevalscurrent")]
public IActionResult Evaluations(
int speakerId,
bool currentYear) => View();

Si alguna de las claves del diccionario coincide con los parámetros de ruta, esos valores se sustituirán en la
ruta según corresponda. Los demás valores no coincidentes se generarán como parámetros de solicitud.

asp-fragment
El atributo asp-fragment define un fragmento de dirección URL que se anexará a la dirección URL. La
aplicación auxiliar de etiquetas delimitadoras agrega el carácter de almohadilla (#). Observe el siguiente
marcado:

<a asp-controller="Speaker"
asp-action="Evaluations"
asp-fragment="SpeakerEvaluations">Speaker Evaluations</a>

El código HTML generado:

<a href="/Speaker/Evaluations#SpeakerEvaluations">Speaker Evaluations</a>

Las etiquetas hash son útiles al crear aplicaciones del lado cliente. Por ejemplo, se pueden usar para el
marcado y la búsqueda en JavaScript.

asp-area
El atributo asp-area establece el nombre de área que se usa para establecer la ruta adecuada. En el siguiente
ejemplo se muestra cómo el atributo de área provoca una reasignación de rutas. Al establecer asp-area en
"Blogs", el directorio Areas/Blogs se prefija en las rutas de los controladores y vistas asociados de esta etiqueta
delimitadora.
<Nombre del proyecto>
wwwroot
Áreas
Blogs
Controladores
HomeController.cs
Vistas
Página principal
AboutBlog.cshtml
Index.cshtml
_ViewStart.cshtml
Controladores
Dada la jerarquía de directorios anterior, el marcado para hacer referencia al archivo AboutBlog.cshtml es el
siguiente:
<a asp-area="Blogs"
asp-controller="Home"
asp-action="AboutBlog">About Blog</a>

El código HTML generado:

<a href="/Blogs/Home/AboutBlog">About Blog</a>

TIP
Para que las áreas funcionen en una aplicación de MVC, la plantilla de ruta debe incluir una referencia al área, en el caso
de que exista. Esta plantilla se representa mediante el segundo parámetro de la llamada de método routes.MapRoute
en Startup.Configure:[!code-csharp]

asp-protocol
El atributo asp-protocol sirve para especificar un protocolo (por ejemplo, https ) en la dirección URL. Por
ejemplo:

<a asp-protocol="https"
asp-controller="Home"
asp-action="About">About</a>

El código HTML generado:

<a href="https://localhost/Home/About">About</a>

El nombre de host del ejemplo es localhost, pero la aplicación auxiliar de etiquetas delimitadoras usa el
dominio público del sitio web al generar la dirección URL.

asp-host
El atributo asp-host sirve para especificar un nombre de host en la dirección URL. Por ejemplo:

<a asp-protocol="https"
asp-host="microsoft.com"
asp-controller="Home"
asp-action="About">About</a>

El código HTML generado:

<a href="https://microsoft.com/Home/About">About</a>

asp-page
El atributo asp-page se usa con las páginas de Razor. Úselo para establecer el valor del atributo href de una
etiqueta delimitadora en una página específica. La dirección URL se crea al prefijar el nombre de la página con
una barra diagonal ("/").
En el ejemplo siguiente se señala a la página de Razor de asistentes:
<a asp-page="/Attendee">All Attendees</a>

El código HTML generado:

<a href="/Attendee">All Attendees</a>

El atributo asp-page es mutuamente excluyente con los atributos asp-route , asp-controller y asp-action .
Pero se puede usar asp-page con asp-route-{value} para controlar el enrutamiento, como se muestra en el
siguiente marcado:

<a asp-page="/Attendee"
asp-route-attendeeid="10">View Attendee</a>

El código HTML generado:

<a href="/Attendee?attendeeid=10">View Attendee</a>

asp-page-handler
El atributo asp-page-handler se usa con las páginas de Razor. Está diseñado para crear un vínculo con
controladores de página específicos.
Observe el siguiente controlador de página:

public void OnGetProfile(int attendeeId)


{
ViewData["AttendeeId"] = attendeeId;

// code omitted for brevity


}

El marcado asociado del modelo de página se vincula con el controlador de página OnGetProfile . Tenga en
cuenta que el prefijo On<Verb> del nombre de método del controlador de página se omite en el valor del
atributo asp-page-handler . Si se tratara de un método asincrónico, también se omitiría el sufijo Async .

<a asp-page="/Attendee"
asp-page-handler="Profile"
asp-route-attendeeid="12">Attendee Profile</a>

El código HTML generado:

<a href="/Attendee?attendeeid=12&handler=Profile">Attendee Profile</a>

Recursos adicionales
Áreas
Introducción a las páginas de Razor
Aplicación auxiliar de etiquetas de caché en ASP.NET
Core MVC
25/06/2018 • 9 minutes to read • Edit Online

Por Peter Kellner


La aplicación auxiliar de etiqueta de caché proporciona la capacidad para mejorar drásticamente el rendimiento
de la aplicación de ASP.NET Core al permitir almacenar en memoria caché su contenido en el proveedor de
caché interno de ASP.NET Core.
El motor de visualización Razor establece el valor predeterminado expires-after en veinte minutos.
El siguiente marcado de Razor almacena en caché la fecha y hora:

<cache>@DateTime.Now</cache>

La primera solicitud a la página que contiene CacheTagHelper mostrará la fecha y hora actuales. Las solicitudes
adicionales mostrarán el valor almacenado en caché hasta que la memoria caché expira (el valor predeterminado
es 20 minutos) o se expulsa por la presión de memoria.
Puede establecer la duración de la caché con los siguientes atributos:

Atributos de la aplicación auxiliar de etiqueta de caché


enabled
TIPO DE ATRIBUTO VALORES VÁLIDOS

booleano "true" (valor predeterminado)

"false"

Determina si el contenido incluido en la aplicación auxiliar de etiqueta de caché se almacena en caché. El valor
predeterminado es true . Si se establece en false , la aplicación auxiliar de etiqueta de caché no tiene ningún
efecto de almacenamiento en caché en la salida representada.
Ejemplo:

<cache enabled="true">
Current Time Inside Cache Tag Helper: @DateTime.Now
</cache>

expires-on
TIPO DE ATRIBUTO VALOR DE EJEMPLO

DateTimeOffset "@new DateTime(2025,1,29,17,02,0)"

Establece una fecha de expiración absoluta. En el ejemplo siguiente, se almacenará en memoria caché el
contenido de la aplicación auxiliar de etiqueta de caché hasta las 17:02 del 29 de enero de 2025.
Ejemplo:

<cache expires-on="@new DateTime(2025,1,29,17,02,0)">


Current Time Inside Cache Tag Helper: @DateTime.Now
</cache>

expires-after
TIPO DE ATRIBUTO VALOR DE EJEMPLO

TimeSpan "@TimeSpan.FromSeconds(120)"

Establece el período de tiempo desde la primera solicitud para almacenar en caché el contenido.
Ejemplo:

<cache expires-after="@TimeSpan.FromSeconds(120)">
Current Time Inside Cache Tag Helper: @DateTime.Now
</cache>

expires-sliding
TIPO DE ATRIBUTO VALOR DE EJEMPLO

TimeSpan "@TimeSpan.FromSeconds(60)"

Establece el tiempo en que se debe expulsar una entrada de caché si no se ha accedido a ella.
Ejemplo:

<cache expires-sliding="@TimeSpan.FromSeconds(60)">
Current Time Inside Cache Tag Helper: @DateTime.Now
</cache>

vary-by-header
TIPO DE ATRIBUTO VALORES DE EJEMPLO

String "User-Agent"

"User-Agent,content-encoding"

Acepta un valor de encabezado único o una lista separada por comas con los valores de encabezado que
desencadenan una actualización de la caché cuando cambian. En el ejemplo siguiente se supervisa el valor del
encabezado User-Agent . En el ejemplo se almacenará en memoria caché el contenido de cada User-Agent que
se muestre al servidor web.
Ejemplo:
<cache vary-by-header="User-Agent">
Current Time Inside Cache Tag Helper: @DateTime.Now
</cache>

vary-by-query
TIPO DE ATRIBUTO VALORES DE EJEMPLO

String "Make"

"Make,Model"

Acepta un valor de encabezado único o una lista separada por comas con los valores de encabezado que
desencadenan una actualización de la caché cuando cambia el valor de encabezado. En el ejemplo siguiente se
examinan los valores de Make y Model .
Ejemplo:

<cache vary-by-query="Make,Model">
Current Time Inside Cache Tag Helper: @DateTime.Now
</cache>

vary-by-route
TIPO DE ATRIBUTO VALORES DE EJEMPLO

String "Make"

"Make,Model"

Acepta un valor de encabezado único o una lista separada por comas con los valores de encabezado que
desencadenan una actualización de la caché cuando se produce un cambio en los valores de parámetro de datos
de ruta. Ejemplo:
Startup.cs

routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{Make?}/{Model?}");

Index.cshtml

<cache vary-by-route="Make,Model">
Current Time Inside Cache Tag Helper: @DateTime.Now
</cache>

vary-by-cookie
TIPO DE ATRIBUTO VALORES DE EJEMPLO

String ".AspNetCore.Identity.Application"
TIPO DE ATRIBUTO VALORES DE EJEMPLO

".AspNetCore.Identity.Application,HairColor"

Acepta un valor de encabezado único o una lista separada por comas con los valores de encabezado que
desencadenan una actualización de la caché cuando cambian los valores de encabezado. En el ejemplo siguiente
se examina la cookie asociada con ASP.NET Identity. Cuando un usuario se autentica, la cookie de solicitud que se
establece desencadena una actualización de la caché.
Ejemplo:

<cache vary-by-cookie=".AspNetCore.Identity.Application">
Current Time Inside Cache Tag Helper: @DateTime.Now
</cache>

vary-by-user
TIPO DE ATRIBUTO VALORES DE EJEMPLO

Booleano "true"

"false" (valor predeterminado)

Especifica si debe restablecerse la memoria caché cuando el usuario que ha iniciado la sesión (o la entidad de
seguridad del contexto) cambia. El usuario actual también se conoce como entidad de seguridad del contexto de
solicitud y puede verse en una vista Razor mediante una referencia a @User.Identity.Name .
En el ejemplo siguiente se examina el usuario conectado actualmente.
Ejemplo:

<cache vary-by-user="true">
Current Time Inside Cache Tag Helper: @DateTime.Now
</cache>

Con este atributo, se mantiene el contenido en caché a través de un ciclo de inicio y cierre de sesión. Al utilizar
vary-by-user="true" , una acción de inicio y cierre de sesión invalida la caché para el usuario autenticado. Se
invalida la memoria caché porque se genera un nuevo valor único de cookie al iniciar sesión. Se mantiene la
memoria caché para el estado anónimo cuando no hay ninguna cookie o la cookie ha expirado. Esto significa que
si ningún usuario ha iniciado sesión, se mantendrá la memoria caché.

vary-by
TIPO DE ATRIBUTO VALORES DE EJEMPLO

String "@Model"

Permite la personalización de los datos que se almacenan en caché. Cuando el objeto al que hace referencia el
valor de cadena del atributo cambia, el contenido de la aplicación auxiliar de etiqueta de caché se actualiza. A
menudo se asignan a este atributo una concatenación de cadenas de valores del modelo. De hecho, eso significa
que una actualización de cualquiera de los valores concatenados invalida la memoria caché.
En el ejemplo siguiente se supone que el método de controlador que representa la vista suma el valor del entero
de los dos parámetros de ruta, myParam1 y myParam2 , y devuelve el resultado como la propiedad de modelo
simple. Cuando se cambia esta suma, el contenido de la aplicación auxiliar de etiqueta de caché se representa y
almacena en caché de nuevo.
Ejemplo:
Acción:

public IActionResult Index(string myParam1,string myParam2,string myParam3)


{
int num1;
int num2;
int.TryParse(myParam1, out num1);
int.TryParse(myParam2, out num2);
return View(viewName, num1 + num2);
}

Index.cshtml

<cache vary-by="@Model"">
Current Time Inside Cache Tag Helper: @DateTime.Now
</cache>

priority
TIPO DE ATRIBUTO VALORES DE EJEMPLO

CacheItemPriority "High"

"Low"

"NeverRemove"

"Normal"

Proporciona instrucciones de expulsión de caché para el proveedor de caché integrado. El servidor web expulsará
primero las entradas de caché Low cuando esté bajo presión de memoria.
Ejemplo:

<cache priority="High">
Current Time Inside Cache Tag Helper: @DateTime.Now
</cache>

El atributo priority no garantiza un nivel específico de retención de la memoria caché. CacheItemPriority es


solo una sugerencia. Establecer este atributo en NeverRemove no garantiza que siempre se conservará la
memoria caché. Para más información, consulte Recursos adicionales.
La aplicación auxiliar de etiqueta de caché es dependiente del servicio de caché de memoria. La aplicación
auxiliar de etiqueta de caché agrega el servicio si no se ha agregado.

Recursos adicionales
Almacenamiento en caché en memoria
Introducción a Identity
Aplicación auxiliar de etiquetas de caché distribuida
en ASP.NET Core
25/06/2018 • 3 minutes to read • Edit Online

Por Peter Kellner


La aplicación auxiliar de etiquetas de caché distribuida proporciona la capacidad de mejorar drásticamente el
rendimiento de la aplicación ASP.NET Core al permitir almacenar en caché su contenido en un origen de caché
distribuida.
La aplicación auxiliar de etiquetas de caché distribuida hereda de la misma clase base que la aplicación auxiliar de
etiquetas de caché. Todos los atributos asociados a la aplicación auxiliar de etiquetas de caché también
funcionarán en la aplicación auxiliar de etiquetas de caché distribuida.
La aplicación auxiliar de etiquetas de caché distribuida sigue el principio de dependencias explícitas conocido
como inserción de constructores. En concreto, el contenedor de interfaz IDistributedCache se pasa al
constructor de la aplicación auxiliar de etiquetas de caché distribuida. Si no se ha creado ninguna implementación
específica de IDistributedCache en ConfigureServices , que normalmente se encuentra en startup.cs, la
aplicación auxiliar de etiquetas de caché distribuida usará el mismo proveedor en memoria para almacenar datos
en caché que la aplicación auxiliar de etiquetas de caché básica.

Atributos de la aplicación auxiliar de etiquetas de caché distribuida


enabled expires-on expires-after expires-sliding vary-by-header vary-by-query vary-by-route vary-by-cookie
vary-by-user vary-by priority
Vea la aplicación auxiliar de etiquetas de caché para obtener las definiciones. La aplicación auxiliar de etiquetas de
caché distribuida hereda de la misma clase que la aplicación auxiliar de etiquetas de caché, de modo que todos
estos atributos son iguales que los de la aplicación auxiliar de etiquetas de caché.

Atributo name (obligatorio )


TIPO DE ATRIBUTO VALOR DE EJEMPLO

cadena "my-distributed-cache-unique-key-101"

El atributo name obligatorio se usa como clave de la caché almacenada para cada instancia de una aplicación
auxiliar de etiquetas de caché distribuida. A diferencia de la aplicación auxiliar de etiquetas de caché básica, que
asigna una clave a cada instancia de la aplicación auxiliar de etiquetas de caché en función del nombre de la
página de Razor y la ubicación de la aplicación auxiliar de etiquetas en la página de Razor, la aplicación auxiliar de
etiquetas de caché distribuida solo basa su clave en el atributo name .
Ejemplo de uso:

<distributed-cache name="my-distributed-cache-unique-key-101">
Time Inside Cache Tag Helper: @DateTime.Now
</distributed-cache>

Implementaciones de IDistributedCache de la aplicación auxiliar de


etiquetas de caché distribuida
Hay dos implementaciones de IDistributedCache integradas en ASP.NET Core. Una se basa en SQL Server y la
otra, en Redis. En Trabajar con una memoria caché distribuida en ASP.NET Core encontrará detalles de estas
implementaciones. Ambas implementaciones requieren establecer una instancia de IDistributedCache en el
archivo Startup.cs de ASP.NET Core.
No hay atributos de etiqueta asociados específicamente con el uso de implementaciones concretas de
IDistributedCache .

Recursos adicionales
Aplicación auxiliar de etiquetas de caché en ASP.NET Core MVC
Inserción de dependencias en ASP.NET Core
Trabajar con una memoria caché distribuida en ASP.NET Core
Almacenar en memoria caché en memoria en el núcleo de ASP.NET
Introducción a la identidad de un núcleo de ASP.NET
Aplicación auxiliar de etiquetas de entorno en
ASP.NET Core
25/06/2018 • 2 minutes to read • Edit Online

Por Peter Kellner y Hisham Bin Ateya


La aplicación auxiliar de etiquetas de entorno representa condicionalmente el contenido incluido en función del
entorno de hospedaje actual. Su único atributo names es una lista separada por comas de nombres de entorno
que, en caso de que alguno coincida con el entorno actual, hará que se represente el contenido incluido.

Atributos de aplicación auxiliar de etiquetas de entorno


nombres
Acepta un solo nombre de entorno de hospedaje o una lista separada por comas de nombres de entorno de
hospedaje que desencadenan la representación del contenido incluido.
Estos valores se comparan con el valor actual devuelto desde la propiedad estática
HostingEnvironment.EnvironmentName de ASP.NET Core. Este valor es uno de los siguientes: Staging,
Development o Production. La comparación ignora el uso de mayúsculas y minúsculas.
Un ejemplo de una aplicación auxiliar de etiquetas environment válida es el siguiente:

<environment names="Staging,Production">
<strong>HostingEnvironment.EnvironmentName is Staging or Production</strong>
</environment>

Atributos include y exclude


ASP.NET Core 2.x agrega los atributos include & exclude . Estos atributos controlan la representación del
contenido incluido en función de los nombres de entorno de hospedaje incluidos o excluidos.
Propiedad include de ASP.NET 2.0 y versiones posteriores
La propiedad include tiene un comportamiento similar al del atributo names en ASP.NET Core 1.0.

<environment include="Staging,Production">
<strong>HostingEnvironment.EnvironmentName is Staging or Production</strong>
</environment>

Propiedad exclude de ASP.NET Core 2.0 y versiones posteriores


En cambio, la propiedad exclude permite que EnvironmentTagHelper represente el contenido incluido para todos
los nombres de entorno de hospedaje, excepto los que haya especificado.

<environment exclude="Development">
<strong>HostingEnvironment.EnvironmentName is Staging or Production</strong>
</environment>

Recursos adicionales
Usar varios entornos en ASP.NET Core
Inserción de dependencias en ASP.NET Core
Aplicaciones auxiliares de etiquetas en formularios de
ASP.NET Core
19/06/2018 • 29 minutes to read • Edit Online

Por Rick Anderson, Dave Paquette y Jerrie Pelser


En este documento se explica cómo trabajar con formularios y se detallan los elementos HTML que se usan
habitualmente en un formulario. El elemento HTML Form proporciona el mecanismo principal que las aplicaciones
web usan a la hora de devolver datos al servidor. La mayor parte de este documento se centra en describir las
aplicaciones auxiliares de etiquetas y cómo pueden servir para crear formularios HTML eficaces de manera
productiva. Se recomienda leer Introduction to Tag Helpers (Introducción a las aplicaciones auxiliares de etiquetas)
antes de este documento.
En muchos casos, las aplicaciones auxiliares HTML proporcionan un método alternativo a una aplicación auxiliar de
etiquetas específica, pero es importante tener en cuenta que las aplicaciones auxiliares de etiquetas no reemplazan
a las aplicaciones auxiliares HTML y que, de igual modo, no hay una aplicación auxiliar de etiqueta para cada
aplicación auxiliar HTML. Si existe una alternativa de aplicación auxiliar HTML, se mencionará aquí.

Aplicación auxiliar de etiquetas de formulario (Form)


La aplicación auxiliar de etiquetas Form hace lo siguiente:
Genera el valor de atributo action del elemento HTML <FORM> de una acción de controlador MVC o ruta
con nombre.
Genera un token comprobación de solicitudes oculto que impide que se falsifiquen solicitudes entre sitios
(cuando se usa con el atributo [ValidateAntiForgeryToken] en el método de acción HTTP Post).
Proporciona el atributo asp-route-<Parameter Name> , donde <Parameter Name> se agrega a los valores de
ruta. Los parámetros routeValues de Html.BeginForm y Html.BeginRouteForm proporcionan una
funcionalidad similar.
Tiene Html.BeginForm y Html.BeginRouteForm como alternativa de aplicación auxiliar HTML.
Ejemplo:

<form asp-controller="Demo" asp-action="Register" method="post">


<!-- Input and Submit elements -->
</form>

La aplicación auxiliar de etiquetas Form anterior genera el siguiente HTML:

<form method="post" action="/Demo/Register">


<!-- Input and Submit elements -->
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

El tiempo de ejecución MVC genera el valor de atributo action de los atributos de aplicación auxiliar de etiquetas
Form asp-controller y asp-action . La aplicación auxiliar de etiquetas Form genera también un token de
comprobación de solicitudes oculto que impide que se falsifiquen solicitudes entre sitios (cuando se usa con el
atributo [ValidateAntiForgeryToken] en el método de acción HTTP Post). Proteger un elemento HTML Form puro
de la falsificación de solicitudes entre sitios no es tarea fácil, y la aplicación auxiliar de etiquetas Form presta este
servicio.
Uso de una ruta con nombre
El atributo de aplicación auxiliar de etiquetas asp-route puede generar también el marcado del atributo HTML
action . Una aplicación con una ruta denominada register podría usar el siguiente marcado para la página de
registro:

<form asp-route="register" method="post">


<!-- Input and Submit elements -->
</form>

Muchas de las vistas de la carpeta Views/Account (que se genera cuando se crea una aplicación web con Cuentas
de usuario individuales) contienen el atributo asp-route-returnurl:

<form asp-controller="Account" asp-action="Login"


asp-route-returnurl="@ViewData["ReturnUrl"]"
method="post" class="form-horizontal" role="form">

NOTE
Con las plantillas integradas, returnUrl se rellena automáticamente solo cuando alguien intenta obtener acceso a un
recurso autorizado, pero no se ha autenticado o no tiene autorización. Si se intenta realizar un acceso no autorizado, el
middleware de seguridad redirige a la página de inicio de sesión con returnUrl configurado.

Aplicación auxiliar de etiquetas de entrada (Input)


La aplicación auxiliar de etiquetas Input enlaza un elemento HTML <input> a una expresión de modelo en la vista
de Razor.
Sintaxis:

<input asp-for="<Expression Name>" />

La aplicación auxiliar de etiquetas Input hace lo siguiente:


Genera los atributos HTML y name del nombre de expresión especificado en el atributo asp-for .
id
asp-for="Property1.Property2" es equivalente a m => m.Property1.Property2 . El nombre de una expresión es
lo que se usa para el valor de atributo asp-for . Vea la sección Nombres de expresión para obtener más
información.
Establece el valor de atributo HTML type según los atributos de tipo de modelo y de anotación de datos
aplicados a la propiedad de modelo.
No sobrescribirá el valor de atributo HTML type cuando se especifique uno.
Genera atributos de validación HTML5 a partir de los atributos de anotación de datos aplicados a las
propiedades de modelo.
Tiene características de aplicación auxiliar HTML que se superponen a Html.TextBoxFor y Html.EditorFor .
Vea la sección Alternativas de aplicación auxiliar HTML a la aplicación auxiliar de etiquetas Input
para obtener más información.
Permite establecer tipado fuerte. Si el nombre de la propiedad cambia y no se actualiza la aplicación auxiliar
de etiquetas, aparecerá un error similar al siguiente:

An error occurred during the compilation of a resource required to process


this request. Please review the following specific error details and modify
your source code appropriately.

Type expected
'RegisterViewModel' does not contain a definition for 'Email' and no
extension method 'Email' accepting a first argument of type 'RegisterViewModel'
could be found (are you missing a using directive or an assembly reference?)

La aplicación auxiliar de etiquetas Input establece el atributo HTML type en función del tipo .NET. En la siguiente
tabla se enumeran algunos tipos .NET habituales y el tipo HTML generado correspondiente (no incluimos aquí
todos los tipos .NET).

TIPO DE .NET TIPO DE ENTRADA

Bool type=”checkbox”

String type=”text”

DateTime type=”datetime”

Byte type=”number”

Valor int. type=”number”

Single, Double type=”number”

En la siguiente tabla se muestran algunos atributos de anotación de datos comunes que la aplicación auxiliar de
etiquetas Input asignará a tipos de entrada concretos (no incluimos aquí todos los atributo de validación):

ATRIBUTO TIPO DE ENTRADA

[EmailAddress] type=”email”

[Url] type=”url”

[HiddenInput] type=”hidden”

[Phone] type=”tel”

[DataType(DataType.Password)] type=”password”

[DataType(DataType.Date)] type=”date”

[DataType(DataType.Time)] type=”time”

Ejemplo:
using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }

[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
}

@model RegisterViewModel

<form asp-controller="Demo" asp-action="RegisterInput" method="post">


Email: <input asp-for="Email" /> <br />
Password: <input asp-for="Password" /><br />
<button type="submit">Register</button>
</form>

El código anterior genera el siguiente HTML:

<form method="post" action="/Demo/RegisterInput">


Email:
<input type="email" data-val="true"
data-val-email="The Email Address field is not a valid email address."
data-val-required="The Email Address field is required."
id="Email" name="Email" value="" /> <br>
Password:
<input type="password" data-val="true"
data-val-required="The Password field is required."
id="Password" name="Password" /><br>
<button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

Las anotaciones de datos que se aplican a las propiedades Email y Password generan metadatos en el modelo. La
aplicación auxiliar de etiquetas Input usa esos metadatos del modelo y genera atributos HTML5 data-val-* . Vea
Model Validation (Validación del modelo). Estos atributos describen los validadores que se van a adjuntar a los
campos de entrada, lo que proporciona HTML5 discreto y validación de jQuery. Los atributos discretos tienen el
formato data-val-rule="Error Message" , donde "rule" es el nombre de la regla de validación (como
data-val-required , data-val-email , data-val-maxlength , etc.). Si aparece un mensaje de error en el atributo, se
mostrará como el valor del atributo data-val-rule . También hay atributos con el formato
data-val-ruleName-argumentName="argumentValue" que aportan más información sobre la regla, por ejemplo,
data-val-maxlength-max="1024" .

Alternativas de aplicación auxiliar HTML a la aplicación auxiliar de etiquetas Input


Html.TextBox , Html.TextBoxFor , Html.Editor y Html.EditorFor tienen características que se superponen a la
aplicación auxiliar de etiquetas Input. La aplicación auxiliar de etiquetas Input establecerá automáticamente el
atributo type , cosa que no ocurrirá con Html.TextBox ni Html.TextBoxFor . Html.Editor y Html.EditorFor
controlan colecciones, objetos complejos y plantillas, pero la aplicación auxiliar de etiquetas Input no. La aplicación
auxiliar de etiquetas Input, Html.EditorFor y Html.TextBoxFor están fuertemente tipados (usan expresiones
lambda), pero Html.TextBox y Html.Editor no (usan nombres de expresión).
HtmlAttributes
@Html.Editor() y @Html.EditorFor() usan una entrada ViewDataDictionary especial denominada htmlAttributes
al ejecutar sus plantillas predeterminadas. Si lo desea, este comportamiento se puede enriquecer con parámetros
additionalViewData . En la clave "htmlAttributes" se distingue entre mayúsculas y minúsculas. La clave
"htmlAttributes" se controla de forma similar al objeto htmlAttributes pasado a aplicaciones auxiliares de etiqueta
Input como @Html.TextBox() .

@Html.EditorFor(model => model.YourProperty,


new { htmlAttributes = new { @class="myCssClass", style="Width:100px" } })

Nombres de expresión
El valor del atributo asp-for es una ModelExpression y la parte de la derecha de una expresión lambda. Por tanto,
asp-for="Property1" se convierte en m => m.Property1 en el código generado, motivo por el que no es necesario
incluir el prefijo Model . Puede usar el carácter "@" para iniciar una expresión insertada y moverla antes de `m.`:
</span><span class="sxs-lookup">You can use the "@" character to start an inline expression and move before
the m. :

@{
var joe = "Joe";
}
<input asp-for="@joe" />

Se genera el siguiente HTML:

<input type="text" id="joe" name="joe" value="Joe" />

Con las propiedades de colección, asp-for="CollectionProperty[23].Member" genera el mismo nombre que


asp-for="CollectionProperty[i].Member" si i tiene el valor 23 .
Cuando ASP.NET Core MVC calcula el valor de ModelExpression , inspecciona varios orígenes, ModelState incluido.
Fíjese en <input type="text" asp-for="@Name" /> . El atributo value calculado es el primer valor distinto de null de:
La entrada ModelState con la clave "Name".
El resultado de la expresión Model.Name .
Navegar a las propiedades secundarias
También se puede navegar a las propiedades secundarias a través de la ruta de acceso de propiedades del modelo
de vista. Pensemos en una clase de modelo más compleja que contiene una propiedad secundaria Address .

public class AddressViewModel


{
public string AddressLine1 { get; set; }
}
public class RegisterAddressViewModel
{
public string Email { get; set; }

[DataType(DataType.Password)]
public string Password { get; set; }

public AddressViewModel Address { get; set; }


}

En la vista, enlazamos a Address.AddressLine1 :

@model RegisterAddressViewModel

<form asp-controller="Demo" asp-action="RegisterAddress" method="post">


Email: <input asp-for="Email" /> <br />
Password: <input asp-for="Password" /><br />
Address: <input asp-for="Address.AddressLine1" /><br />
<button type="submit">Register</button>
</form>

Se genera el siguiente código HTML para Address.AddressLine1 :

<input type="text" id="Address_AddressLine1" name="Address.AddressLine1" value="" />

Colecciones y nombres de expresión


Como ejemplo, un modelo que contiene una matriz de Colors :

public class Person


{
public List<string> Colors { get; set; }

public int Age { get; set; }


}

El método de acción:

public IActionResult Edit(int id, int colorIndex)


{
ViewData["Index"] = colorIndex;
return View(GetPerson(id));
}

El siguiente código de Razor muestra cómo se tiene acceso a un elemento Color concreto:

@model Person
@{
var index = (int)ViewData["index"];
}

<form asp-controller="ToDo" asp-action="Edit" method="post">


@Html.EditorFor(m => m.Colors[index])
<label asp-for="Age"></label>
<input asp-for="Age" /><br />
<button type="submit">Post</button>
</form>
La plantilla Views/Shared/EditorTemplates/String.cshtml:

@model string

<label asp-for="@Model"></label>
<input asp-for="@Model" /> <br />

Ejemplo en el que se usa List<T> :

public class ToDoItem


{
public string Name { get; set; }

public bool IsDone { get; set; }


}

El siguiente código de Razor muestra cómo iterar por una colección:

@model List<ToDoItem>

<form asp-controller="ToDo" asp-action="Edit" method="post">


<table>
<tr> <th>Name</th> <th>Is Done</th> </tr>

@for (int i = 0; i < Model.Count; i++)


{
<tr>
@Html.EditorFor(model => model[i])
</tr>
}

</table>
<button type="submit">Save</button>
</form>

La plantilla Views/Shared/EditorTemplates/ToDoItem.cshtml:

@model ToDoItem

<td>
<label asp-for="@Model.Name"></label>
@Html.DisplayFor(model => model.Name)
</td>
<td>
<input asp-for="@Model.IsDone" />
</td>

@*
This template replaces the following Razor which evaluates the indexer three times.
<td>
<label asp-for="@Model[i].Name"></label>
@Html.DisplayFor(model => model[i].Name)
</td>
<td>
<input asp-for="@Model[i].IsDone" />
</td>
*@
NOTE
Use siempre for (y no foreach ) para iterar por una lista. Evaluar un indizador en una expresión de LINQ puede ser
costoso, con lo cual esa posibilidad hay que reducirla al mínimo.

NOTE
El código de ejemplo comentado anterior muestra cómo reemplazaríamos la expresión lambda por el operador @ para tener
acceso a cada elemento ToDoItem de la lista.

Aplicación auxiliar de etiquetas de área de texto (Textarea)


La aplicación auxiliar de etiquetas Textarea Tag Helper es similar a la aplicación auxiliar de etiquetas Input.
Genera los atributos id y name , y los atributos de validación de datos del modelo de un elemento
<textarea>.
Permite establecer tipado fuerte.
Alternativa de aplicación auxiliar HTML: Html.TextAreaFor .

Ejemplo:

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class DescriptionViewModel
{
[MinLength(5)]
[MaxLength(1024)]
public string Description { get; set; }
}
}

@model DescriptionViewModel

<form asp-controller="Demo" asp-action="RegisterTextArea" method="post">


<textarea asp-for="Description"></textarea>
<button type="submit">Test</button>
</form>

Se genera el siguiente código HTML:


<form method="post" action="/Demo/RegisterTextArea">
<textarea data-val="true"
data-val-maxlength="The field Description must be a string or array type with a maximum length of
&#x27;1024&#x27;."
data-val-maxlength-max="1024"
data-val-minlength="The field Description must be a string or array type with a minimum length of
&#x27;5&#x27;."
data-val-minlength-min="5"
id="Description" name="Description">
</textarea>
<button type="submit">Test</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

Aplicación auxiliar de etiquetas Label


Genera el título de la etiqueta y el atributo for en un elemento de un nombre de expresión.
Alternativa de aplicación auxiliar HTML: Html.LabelFor .

Label Tag Helper proporciona las siguientes ventajas con respecto a un elemento HTML Label puro:
Obtendrá automáticamente el valor de la etiqueta descriptiva del atributo Display . El nombre para mostrar
que se busca puede cambiar con el tiempo y la combinación del atributo Display , y la aplicación auxiliar de
etiquetas Label aplicará el elemento Display en cualquier lugar donde se use.
El código fuente contiene menos marcado.
Permite establecer tipado fuerte con la propiedad de modelo.
Ejemplo:

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class SimpleViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }
}
}

@model SimpleViewModel

<form asp-controller="Demo" asp-action="RegisterLabel" method="post">


<label asp-for="Email"></label>
<input asp-for="Email" /> <br />
</form>

Se genera el siguiente código HTML para el elemento <label> :

<label for="Email">Email Address</label>

La aplicación auxiliar de etiquetas Label genera el valor de atributo for de "Email", que es el identificador asociado
al elemento <input> . Las aplicaciones auxiliares de etiqueta generan elementos id y for coherentes para que se
puedan asociar correctamente. El título de este ejemplo proviene del atributo Display . Si el modelo no contuviera
un atributo Display , el título sería el nombre de propiedad de la expresión.

Aplicaciones auxiliares de etiquetas de validación


Hay dos aplicaciones auxiliares de etiquetas de validación: Validation Message Tag Helper (que muestra un mensaje
de validación relativo a una única propiedad del modelo) y Validation Summary Tag Helper (que muestra un
resumen de los errores de validación). Input Tag Helper agrega atributos de validación del lado cliente HTML5 a
los elementos de entrada en función de los atributos de anotación de datos de las clases del modelo. La validación
también se realiza en el lado servidor. La aplicación auxiliar de etiquetas de validación muestra estos mensajes de
error cuando se produce un error de validación.
Aplicación auxiliar de etiquetas de mensaje de validación
Agrega el atributo HTML5 data-valmsg-for="property" al elemento span, que adjunta los mensajes de error
de validación en el campo de entrada de la propiedad de modelo especificada. Cuando se produce un error
de validación en el lado cliente, jQuery muestra el mensaje de error en el elemento <span> .
La validación también tiene lugar en el lado servidor. Puede que JavaScript esté deshabilitado en los clientes,
mientras que hay algunas validaciones que solo se pueden realizar en el lado servidor.
Alternativa de aplicación auxiliar HTML: Html.ValidationMessageFor .

Validation Message Tag Helper se usa con el atributo asp-validation-for en un elemento HTML span.

<span asp-validation-for="Email"></span>

La aplicación auxiliar de etiquetas de mensaje de validación generará el siguiente código HTML:

<span class="field-validation-valid"
data-valmsg-for="Email"
data-valmsg-replace="true"></span>

Validation Message Tag Helper suele usarse por lo general después de una aplicación auxiliar de etiquetas Input
en la misma propiedad. Gracias a esto, se mostrarán todos los mensajes de error de validación cerca de la entrada
que produjo el error.

NOTE
Debe tener una vista con las referencias de script de JavaScript y de jQuery adecuadas para la validación del lado cliente. Para
más información, vea Introduction to model validation in ASP.NET Core MVC (Introducción a la validación de modelos en
ASP.NET Core MVC).

Cuando se produce un error de validación del lado servidor (por ejemplo, porque haya una validación del lado
servidor personalizada o porque la validación del lado cliente esté deshabilitada), MVC pone ese mensaje de error
como cuerpo del elemento <span> .

<span class="field-validation-error" data-valmsg-for="Email"


data-valmsg-replace="true">
The Email Address field is required.
</span>

Aplicación auxiliar de etiquetas de resumen de validación


Tiene como destino todos los elementos <div> con el atributo asp-validation-summary .
Alternativa de aplicación auxiliar HTML: @Html.ValidationSummary .

Validation Summary Tag Helper se usa para mostrar un resumen de los mensajes de validación. El valor de atributo
asp-validation-summary puede ser cualquiera de los siguientes:

ASP-VALIDATION-SUMMARY MENSAJES DE VALIDACIÓN QUE SE MUESTRAN

ValidationSummary.All Nivel de modelo y de propiedad

ValidationSummary.ModelOnly Modelo

ValidationSummary.None Ninguna

Ejemplo
En el siguiente ejemplo, el modelo de datos se complementa con atributos DataAnnotation , lo que genera mensajes
de error de validación sobre el elemento <input> . Cuando se produce un error de validación, la aplicación auxiliar
de etiquetas de validación muestra el mensaje de error:

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }

[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
}

@model RegisterViewModel

<form asp-controller="Demo" asp-action="RegisterValidation" method="post">


<div asp-validation-summary="ModelOnly"></div>
Email: <input asp-for="Email" /> <br />
<span asp-validation-for="Email"></span><br />
Password: <input asp-for="Password" /><br />
<span asp-validation-for="Password"></span><br />
<button type="submit">Register</button>
</form>

El código HTML generado (cuando el modelo es válido) es este:


<form action="/DemoReg/Register" method="post">
<div class="validation-summary-valid" data-valmsg-summary="true">
<ul><li style="display:none"></li></ul></div>
Email: <input name="Email" id="Email" type="email" value=""
data-val-required="The Email field is required."
data-val-email="The Email field is not a valid email address."
data-val="true"> <br>
<span class="field-validation-valid" data-valmsg-replace="true"
data-valmsg-for="Email"></span><br>
Password: <input name="Password" id="Password" type="password"
data-val-required="The Password field is required." data-val="true"><br>
<span class="field-validation-valid" data-valmsg-replace="true"
data-valmsg-for="Password"></span><br>
<button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

Aplicación auxiliar de etiquetas de selección (Select)


Genera el elemento select y el elemento asociado option de las propiedades del modelo.
Tiene Html.DropDownListFor y Html.ListBoxFor como alternativa de aplicación auxiliar HTML.
El elemento asp-for de Select Tag Helper especifica el nombre de la propiedad de modelo del elemento select,
mientras que asp-items especifica los elementos option. Por ejemplo:

<select asp-for="Country" asp-items="Model.Countries"></select>

Ejemplo:

using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace FormsTagHelper.ViewModels
{
public class CountryViewModel
{
public string Country { get; set; }

public List<SelectListItem> Countries { get; } = new List<SelectListItem>


{
new SelectListItem { Value = "MX", Text = "Mexico" },
new SelectListItem { Value = "CA", Text = "Canada" },
new SelectListItem { Value = "US", Text = "USA" },
};
}
}

El método Index inicializa CountryViewModel , establece el país seleccionado y lo pasa a la vista Index .

public IActionResult IndexOption(int id)


{
var model = new CountryViewModel();
model.Country = "CA";
return View(model);
}

El método HTTP POST Index muestra la selección:


[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Index(CountryViewModel model)
{
if (ModelState.IsValid)
{
var msg = model.Country + " selected";
return RedirectToAction("IndexSuccess", new { message = msg});
}

// If we got this far, something failed; redisplay form.


return View(model);
}

La vista Index :

@model CountryViewModel

<form asp-controller="Home" asp-action="Index" method="post">


<select asp-for="Country" asp-items="Model.Countries"></select>
<br /><button type="submit">Register</button>
</form>

Genera el siguiente código HTML (con la selección "CA"):

<form method="post" action="/">


<select id="Country" name="Country">
<option value="MX">Mexico</option>
<option selected="selected" value="CA">Canada</option>
<option value="US">USA</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

NOTE
No se recomienda usar ViewBag o ViewData con la aplicación auxiliar de etiquetas Select. Un modelo de vista es más eficaz
a la hora de proporcionar metadatos MVC y suele ser menos problemático.

El valor de atributo asp-for es un caso especial y no necesita un prefijo Model , mientras que los otros atributos de
aplicación auxiliar de etiquetas sí (como asp-items ).

<select asp-for="Country" asp-items="Model.Countries"></select>

Enlace con enum


A menudo, conviene usar <select> con una propiedad enum y generar los elementos SelectListItem a partir de
valores enum .
Ejemplo:
public class CountryEnumViewModel
{
public CountryEnum EnumCountry { get; set; }
}
}

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public enum CountryEnum
{
[Display(Name = "United Mexican States")]
Mexico,
[Display(Name = "United States of America")]
USA,
Canada,
France,
Germany,
Spain
}
}

El método GetEnumSelectList genera un objeto SelectList para una enumeración.

@model CountryEnumViewModel

<form asp-controller="Home" asp-action="IndexEnum" method="post">


<select asp-for="EnumCountry"
asp-items="Html.GetEnumSelectList<CountryEnum>()"> >
</select>
<br /><button type="submit">Register</button>
</form>

Puede complementar la lista de enumeradores con el atributo Display para obtener una interfaz de usuario más
completa:

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public enum CountryEnum
{
[Display(Name = "United Mexican States")]
Mexico,
[Display(Name = "United States of America")]
USA,
Canada,
France,
Germany,
Spain
}
}

Se genera el siguiente código HTML:


<form method="post" action="/Home/IndexEnum">
<select data-val="true" data-val-required="The EnumCountry field is required."
id="EnumCountry" name="EnumCountry">
<option value="0">United Mexican States</option>
<option value="1">United States of America</option>
<option value="2">Canada</option>
<option value="3">France</option>
<option value="4">Germany</option>
<option selected="selected" value="5">Spain</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

Agrupamiento de opciones
El elemento HTML <optgroup> se genera cuando el modelo de vista contiene uno o varios objetos
SelectListGroup .

CountryViewModelGroup agrupa los elementos SelectListItem en los grupos "North America" y "Europe":
public class CountryViewModelGroup
{
public CountryViewModelGroup()
{
var NorthAmericaGroup = new SelectListGroup { Name = "North America" };
var EuropeGroup = new SelectListGroup { Name = "Europe" };

Countries = new List<SelectListItem>


{
new SelectListItem
{
Value = "MEX",
Text = "Mexico",
Group = NorthAmericaGroup
},
new SelectListItem
{
Value = "CAN",
Text = "Canada",
Group = NorthAmericaGroup
},
new SelectListItem
{
Value = "US",
Text = "USA",
Group = NorthAmericaGroup
},
new SelectListItem
{
Value = "FR",
Text = "France",
Group = EuropeGroup
},
new SelectListItem
{
Value = "ES",
Text = "Spain",
Group = EuropeGroup
},
new SelectListItem
{
Value = "DE",
Text = "Germany",
Group = EuropeGroup
}
};
}

public string Country { get; set; }

public List<SelectListItem> Countries { get; }

Aquí mostramos los dos grupos:


El código HTML generado:

<form method="post" action="/Home/IndexGroup">


<select id="Country" name="Country">
<optgroup label="North America">
<option value="MEX">Mexico</option>
<option value="CAN">Canada</option>
<option value="US">USA</option>
</optgroup>
<optgroup label="Europe">
<option value="FR">France</option>
<option value="ES">Spain</option>
<option value="DE">Germany</option>
</optgroup>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

Selección múltiple
La aplicación auxiliar de etiquetas Select generará automáticamente el atributo multiple = "multiple" si la propiedad
especificada en el atributo asp-for es IEnumerable . Por ejemplo, si tenemos el siguiente modelo:

using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace FormsTagHelper.ViewModels
{
public class CountryViewModelIEnumerable
{
public IEnumerable<string> CountryCodes { get; set; }

public List<SelectListItem> Countries { get; } = new List<SelectListItem>


{
new SelectListItem { Value = "MX", Text = "Mexico" },
new SelectListItem { Value = "CA", Text = "Canada" },
new SelectListItem { Value = "US", Text = "USA" },
new SelectListItem { Value = "FR", Text = "France" },
new SelectListItem { Value = "ES", Text = "Spain" },
new SelectListItem { Value = "DE", Text = "Germany"}
};
}
}

Con la siguiente vista:


@model CountryViewModelIEnumerable

<form asp-controller="Home" asp-action="IndexMultiSelect" method="post">


<select asp-for="CountryCodes" asp-items="Model.Countries"></select>
<br /><button type="submit">Register</button>
</form>

Se genera el siguiente código HTML:

<form method="post" action="/Home/IndexMultiSelect">


<select id="CountryCodes"
multiple="multiple"
name="CountryCodes"><option value="MX">Mexico</option>
<option value="CA">Canada</option>
<option value="US">USA</option>
<option value="FR">France</option>
<option value="ES">Spain</option>
<option value="DE">Germany</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

Sin selección
Si ve que usa la opción "sin especificar" en varias páginas, puede crear una plantilla para no tener que repetir el
código HTML:

@model CountryViewModel

<form asp-controller="Home" asp-action="IndexEmpty" method="post">


@Html.EditorForModel()
<br /><button type="submit">Register</button>
</form>

La plantilla Views/Shared/EditorTemplates/CountryViewModel.cshtml:

@model CountryViewModel

<select asp-for="Country" asp-items="Model.Countries">


<option value="">--none--</option>
</select>

La posibilidad de agregar elementos HTML <option> no se limita exclusivamente a los casos en los que no se
seleccionada nada. Por ejemplo, el método de acción y vista siguientes generarán un código HTML similar al
código anterior:

public IActionResult IndexOption(int id)


{
var model = new CountryViewModel();
model.Country = "CA";
return View(model);
}
@model CountryViewModel

<form asp-controller="Home" asp-action="IndexEmpty" method="post">


<select asp-for="Country">
<option value="">&lt;none&gt;</option>
<option value="MX">Mexico</option>
<option value="CA">Canada</option>
<option value="US">USA</option>
</select>
<br /><button type="submit">Register</button>
</form>

Se seleccionará el elemento <option> correcto (que contenga el atributo selected="selected" ) en función del valor
real de Country .

<form method="post" action="/Home/IndexEmpty">


<select id="Country" name="Country">
<option value="">&lt;none&gt;</option>
<option value="MX">Mexico</option>
<option value="CA" selected="selected">Canada</option>
<option value="US">USA</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

Recursos adicionales
Aplicaciones auxiliares de etiquetas
HTML Form element (Elemento HTML Form)
Request Verification Token (Token de comprobación de solicitudes)
Enlace de modelos
Introduction to model validation in ASP.NET Core MVC (Introducción a la validación de modelos en ASP.NET
Core MVC )
IAttributeAdapter Interface (Interfaz IAttributeAdapter)
Fragmentos de código de este documento
Aplicación auxiliar de etiquetas de imagen en
ASP.NET Core
25/06/2018 • 3 minutes to read • Edit Online

Por Peter Kellner


La aplicación auxiliar de etiquetas de imagen es una mejora de la etiqueta img ( <img> ). Requiere una etiqueta
src , así como el atributo boolean asp-append-version .

Si el origen de la imagen ( src ) es un archivo estático en el servidor web del host, se anexa una cadena única de
bloqueo de la caché como parámetro de consulta a ese origen de la imagen. De este modo, si el archivo en el
servidor web del host cambia, se generará una dirección URL de solicitud única que incluye el parámetro de
solicitud actualizada. La cadena de limpieza de memoria caché es un valor único que representa el valor hash del
archivo de imagen estático.
Si el origen de la imagen ( src ) no es un archivo estático (por ejemplo, es una dirección URL remota o se trata de
un archivo que no existe en el servidor) el atributo src de la etiqueta <img> se genera sin parámetro de cadena
de consulta de limpieza de caché.

Atributos de la aplicación auxiliar de etiquetas de imagen


asp-append-version
Cuando se especifica junto con un atributo src , se invoca la aplicación auxiliar de etiquetas de imagen.
Este es un ejemplo de aplicación auxiliar de etiquetas img válido:

<img src="~/images/asplogo.png"
asp-append-version="true" />

Si el archivo estático existe en el directorio ..wwwroot/images/asplogo.png, el código HTML generado es similar al


siguiente (el valor hash será diferente):

<img
src="/images/asplogo.png?v=Kl_dqr9NVtnMdsM2MUg4qthUnWZm5T1fCEimBPWDNgM"/>

El valor asignado al parámetro v es el valor hash del archivo almacenado en disco. Si el servidor web no es capaz
de obtener acceso de lectura al archivo estático al que se hace referencia, no se agregará ningún parámetro v al
atributo src .

src
Para activar la aplicación auxiliar de etiquetas de imagen, se requiere el atributo src en el elemento <img> .

NOTE
La aplicación auxiliar de etiquetas de imagen usa el proveedor Cache en el servidor web local para almacenar el valor de
Sha512 calculado de un archivo determinado. Si el archivo se vuelve a solicitar, no es necesario volver a calcular Sha512 . La
memoria caché queda invalidada por un monitor del archivo que se adjunta al archivo cuando el valor de Sha512 del
archivo se calcula.
Recursos adicionales
Almacenar en memoria caché en memoria en el núcleo de ASP.NET
Aplicaciones auxiliares de etiquetas en formularios de
ASP.NET Core
19/06/2018 • 29 minutes to read • Edit Online

Por Rick Anderson, Dave Paquette y Jerrie Pelser


En este documento se explica cómo trabajar con formularios y se detallan los elementos HTML que se usan
habitualmente en un formulario. El elemento HTML Form proporciona el mecanismo principal que las aplicaciones
web usan a la hora de devolver datos al servidor. La mayor parte de este documento se centra en describir las
aplicaciones auxiliares de etiquetas y cómo pueden servir para crear formularios HTML eficaces de manera
productiva. Se recomienda leer Introduction to Tag Helpers (Introducción a las aplicaciones auxiliares de etiquetas)
antes de este documento.
En muchos casos, las aplicaciones auxiliares HTML proporcionan un método alternativo a una aplicación auxiliar de
etiquetas específica, pero es importante tener en cuenta que las aplicaciones auxiliares de etiquetas no reemplazan
a las aplicaciones auxiliares HTML y que, de igual modo, no hay una aplicación auxiliar de etiqueta para cada
aplicación auxiliar HTML. Si existe una alternativa de aplicación auxiliar HTML, se mencionará aquí.

Aplicación auxiliar de etiquetas de formulario (Form)


La aplicación auxiliar de etiquetas Form hace lo siguiente:
Genera el valor de atributo action del elemento HTML <FORM> de una acción de controlador MVC o ruta
con nombre.
Genera un token comprobación de solicitudes oculto que impide que se falsifiquen solicitudes entre sitios
(cuando se usa con el atributo [ValidateAntiForgeryToken] en el método de acción HTTP Post).
Proporciona el atributo asp-route-<Parameter Name> , donde <Parameter Name> se agrega a los valores de
ruta. Los parámetros routeValues de Html.BeginForm y Html.BeginRouteForm proporcionan una
funcionalidad similar.
Tiene Html.BeginForm y Html.BeginRouteForm como alternativa de aplicación auxiliar HTML.
Ejemplo:

<form asp-controller="Demo" asp-action="Register" method="post">


<!-- Input and Submit elements -->
</form>

La aplicación auxiliar de etiquetas Form anterior genera el siguiente HTML:

<form method="post" action="/Demo/Register">


<!-- Input and Submit elements -->
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

El tiempo de ejecución MVC genera el valor de atributo action de los atributos de aplicación auxiliar de etiquetas
Form asp-controller y asp-action . La aplicación auxiliar de etiquetas Form genera también un token de
comprobación de solicitudes oculto que impide que se falsifiquen solicitudes entre sitios (cuando se usa con el
atributo [ValidateAntiForgeryToken] en el método de acción HTTP Post). Proteger un elemento HTML Form puro
de la falsificación de solicitudes entre sitios no es tarea fácil, y la aplicación auxiliar de etiquetas Form presta este
servicio.
Uso de una ruta con nombre
El atributo de aplicación auxiliar de etiquetas asp-route puede generar también el marcado del atributo HTML
action . Una aplicación con una ruta denominada register podría usar el siguiente marcado para la página de
registro:

<form asp-route="register" method="post">


<!-- Input and Submit elements -->
</form>

Muchas de las vistas de la carpeta Views/Account (que se genera cuando se crea una aplicación web con Cuentas
de usuario individuales) contienen el atributo asp-route-returnurl:

<form asp-controller="Account" asp-action="Login"


asp-route-returnurl="@ViewData["ReturnUrl"]"
method="post" class="form-horizontal" role="form">

NOTE
Con las plantillas integradas, returnUrl se rellena automáticamente solo cuando alguien intenta obtener acceso a un
recurso autorizado, pero no se ha autenticado o no tiene autorización. Si se intenta realizar un acceso no autorizado, el
middleware de seguridad redirige a la página de inicio de sesión con returnUrl configurado.

Aplicación auxiliar de etiquetas de entrada (Input)


La aplicación auxiliar de etiquetas Input enlaza un elemento HTML <input> a una expresión de modelo en la vista
de Razor.
Sintaxis:

<input asp-for="<Expression Name>" />

La aplicación auxiliar de etiquetas Input hace lo siguiente:


Genera los atributos HTML y name del nombre de expresión especificado en el atributo asp-for .
id
asp-for="Property1.Property2" es equivalente a m => m.Property1.Property2 . El nombre de una expresión es
lo que se usa para el valor de atributo asp-for . Vea la sección Nombres de expresión para obtener más
información.
Establece el valor de atributo HTML type según los atributos de tipo de modelo y de anotación de datos
aplicados a la propiedad de modelo.
No sobrescribirá el valor de atributo HTML type cuando se especifique uno.
Genera atributos de validación HTML5 a partir de los atributos de anotación de datos aplicados a las
propiedades de modelo.
Tiene características de aplicación auxiliar HTML que se superponen a Html.TextBoxFor y Html.EditorFor .
Vea la sección Alternativas de aplicación auxiliar HTML a la aplicación auxiliar de etiquetas Input
para obtener más información.
Permite establecer tipado fuerte. Si el nombre de la propiedad cambia y no se actualiza la aplicación auxiliar
de etiquetas, aparecerá un error similar al siguiente:

An error occurred during the compilation of a resource required to process


this request. Please review the following specific error details and modify
your source code appropriately.

Type expected
'RegisterViewModel' does not contain a definition for 'Email' and no
extension method 'Email' accepting a first argument of type 'RegisterViewModel'
could be found (are you missing a using directive or an assembly reference?)

La aplicación auxiliar de etiquetas Input establece el atributo HTML type en función del tipo .NET. En la siguiente
tabla se enumeran algunos tipos .NET habituales y el tipo HTML generado correspondiente (no incluimos aquí
todos los tipos .NET).

TIPO DE .NET TIPO DE ENTRADA

Bool type=”checkbox”

String type=”text”

DateTime type=”datetime”

Byte type=”number”

Valor int. type=”number”

Single, Double type=”number”

En la siguiente tabla se muestran algunos atributos de anotación de datos comunes que la aplicación auxiliar de
etiquetas Input asignará a tipos de entrada concretos (no incluimos aquí todos los atributo de validación):

ATRIBUTO TIPO DE ENTRADA

[EmailAddress] type=”email”

[Url] type=”url”

[HiddenInput] type=”hidden”

[Phone] type=”tel”

[DataType(DataType.Password)] type=”password”

[DataType(DataType.Date)] type=”date”

[DataType(DataType.Time)] type=”time”

Ejemplo:
using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }

[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
}

@model RegisterViewModel

<form asp-controller="Demo" asp-action="RegisterInput" method="post">


Email: <input asp-for="Email" /> <br />
Password: <input asp-for="Password" /><br />
<button type="submit">Register</button>
</form>

El código anterior genera el siguiente HTML:

<form method="post" action="/Demo/RegisterInput">


Email:
<input type="email" data-val="true"
data-val-email="The Email Address field is not a valid email address."
data-val-required="The Email Address field is required."
id="Email" name="Email" value="" /> <br>
Password:
<input type="password" data-val="true"
data-val-required="The Password field is required."
id="Password" name="Password" /><br>
<button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

Las anotaciones de datos que se aplican a las propiedades Email y Password generan metadatos en el modelo. La
aplicación auxiliar de etiquetas Input usa esos metadatos del modelo y genera atributos HTML5 data-val-* . Vea
Model Validation (Validación del modelo). Estos atributos describen los validadores que se van a adjuntar a los
campos de entrada, lo que proporciona HTML5 discreto y validación de jQuery. Los atributos discretos tienen el
formato data-val-rule="Error Message" , donde "rule" es el nombre de la regla de validación (como
data-val-required , data-val-email , data-val-maxlength , etc.). Si aparece un mensaje de error en el atributo, se
mostrará como el valor del atributo data-val-rule . También hay atributos con el formato
data-val-ruleName-argumentName="argumentValue" que aportan más información sobre la regla, por ejemplo,
data-val-maxlength-max="1024" .

Alternativas de aplicación auxiliar HTML a la aplicación auxiliar de etiquetas Input


Html.TextBox , Html.TextBoxFor , Html.Editor y Html.EditorFor tienen características que se superponen a la
aplicación auxiliar de etiquetas Input. La aplicación auxiliar de etiquetas Input establecerá automáticamente el
atributo type , cosa que no ocurrirá con Html.TextBox ni Html.TextBoxFor . Html.Editor y Html.EditorFor
controlan colecciones, objetos complejos y plantillas, pero la aplicación auxiliar de etiquetas Input no. La aplicación
auxiliar de etiquetas Input, Html.EditorFor y Html.TextBoxFor están fuertemente tipados (usan expresiones
lambda), pero Html.TextBox y Html.Editor no (usan nombres de expresión).
HtmlAttributes
@Html.Editor() y @Html.EditorFor() usan una entrada ViewDataDictionary especial denominada htmlAttributes
al ejecutar sus plantillas predeterminadas. Si lo desea, este comportamiento se puede enriquecer con parámetros
additionalViewData . En la clave "htmlAttributes" se distingue entre mayúsculas y minúsculas. La clave
"htmlAttributes" se controla de forma similar al objeto htmlAttributes pasado a aplicaciones auxiliares de etiqueta
Input como @Html.TextBox() .

@Html.EditorFor(model => model.YourProperty,


new { htmlAttributes = new { @class="myCssClass", style="Width:100px" } })

Nombres de expresión
El valor del atributo asp-for es una ModelExpression y la parte de la derecha de una expresión lambda. Por tanto,
asp-for="Property1" se convierte en m => m.Property1 en el código generado, motivo por el que no es necesario
incluir el prefijo Model . Puede usar el carácter "@" para iniciar una expresión insertada y moverla antes de `m.`:
</span><span class="sxs-lookup">You can use the "@" character to start an inline expression and move before
the m. :

@{
var joe = "Joe";
}
<input asp-for="@joe" />

Se genera el siguiente HTML:

<input type="text" id="joe" name="joe" value="Joe" />

Con las propiedades de colección, asp-for="CollectionProperty[23].Member" genera el mismo nombre que


asp-for="CollectionProperty[i].Member" si i tiene el valor 23 .
Cuando ASP.NET Core MVC calcula el valor de ModelExpression , inspecciona varios orígenes, ModelState incluido.
Fíjese en <input type="text" asp-for="@Name" /> . El atributo value calculado es el primer valor distinto de null de:
La entrada ModelState con la clave "Name".
El resultado de la expresión Model.Name .
Navegar a las propiedades secundarias
También se puede navegar a las propiedades secundarias a través de la ruta de acceso de propiedades del modelo
de vista. Pensemos en una clase de modelo más compleja que contiene una propiedad secundaria Address .

public class AddressViewModel


{
public string AddressLine1 { get; set; }
}
public class RegisterAddressViewModel
{
public string Email { get; set; }

[DataType(DataType.Password)]
public string Password { get; set; }

public AddressViewModel Address { get; set; }


}

En la vista, enlazamos a Address.AddressLine1 :

@model RegisterAddressViewModel

<form asp-controller="Demo" asp-action="RegisterAddress" method="post">


Email: <input asp-for="Email" /> <br />
Password: <input asp-for="Password" /><br />
Address: <input asp-for="Address.AddressLine1" /><br />
<button type="submit">Register</button>
</form>

Se genera el siguiente código HTML para Address.AddressLine1 :

<input type="text" id="Address_AddressLine1" name="Address.AddressLine1" value="" />

Colecciones y nombres de expresión


Como ejemplo, un modelo que contiene una matriz de Colors :

public class Person


{
public List<string> Colors { get; set; }

public int Age { get; set; }


}

El método de acción:

public IActionResult Edit(int id, int colorIndex)


{
ViewData["Index"] = colorIndex;
return View(GetPerson(id));
}

El siguiente código de Razor muestra cómo se tiene acceso a un elemento Color concreto:

@model Person
@{
var index = (int)ViewData["index"];
}

<form asp-controller="ToDo" asp-action="Edit" method="post">


@Html.EditorFor(m => m.Colors[index])
<label asp-for="Age"></label>
<input asp-for="Age" /><br />
<button type="submit">Post</button>
</form>
La plantilla Views/Shared/EditorTemplates/String.cshtml:

@model string

<label asp-for="@Model"></label>
<input asp-for="@Model" /> <br />

Ejemplo en el que se usa List<T> :

public class ToDoItem


{
public string Name { get; set; }

public bool IsDone { get; set; }


}

El siguiente código de Razor muestra cómo iterar por una colección:

@model List<ToDoItem>

<form asp-controller="ToDo" asp-action="Edit" method="post">


<table>
<tr> <th>Name</th> <th>Is Done</th> </tr>

@for (int i = 0; i < Model.Count; i++)


{
<tr>
@Html.EditorFor(model => model[i])
</tr>
}

</table>
<button type="submit">Save</button>
</form>

La plantilla Views/Shared/EditorTemplates/ToDoItem.cshtml:

@model ToDoItem

<td>
<label asp-for="@Model.Name"></label>
@Html.DisplayFor(model => model.Name)
</td>
<td>
<input asp-for="@Model.IsDone" />
</td>

@*
This template replaces the following Razor which evaluates the indexer three times.
<td>
<label asp-for="@Model[i].Name"></label>
@Html.DisplayFor(model => model[i].Name)
</td>
<td>
<input asp-for="@Model[i].IsDone" />
</td>
*@
NOTE
Use siempre for (y no foreach ) para iterar por una lista. Evaluar un indizador en una expresión de LINQ puede ser
costoso, con lo cual esa posibilidad hay que reducirla al mínimo.

NOTE
El código de ejemplo comentado anterior muestra cómo reemplazaríamos la expresión lambda por el operador @ para tener
acceso a cada elemento ToDoItem de la lista.

Aplicación auxiliar de etiquetas de área de texto (Textarea)


La aplicación auxiliar de etiquetas Textarea Tag Helper es similar a la aplicación auxiliar de etiquetas Input.
Genera los atributos id y name , y los atributos de validación de datos del modelo de un elemento
<textarea>.
Permite establecer tipado fuerte.
Alternativa de aplicación auxiliar HTML: Html.TextAreaFor .

Ejemplo:

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class DescriptionViewModel
{
[MinLength(5)]
[MaxLength(1024)]
public string Description { get; set; }
}
}

@model DescriptionViewModel

<form asp-controller="Demo" asp-action="RegisterTextArea" method="post">


<textarea asp-for="Description"></textarea>
<button type="submit">Test</button>
</form>

Se genera el siguiente código HTML:


<form method="post" action="/Demo/RegisterTextArea">
<textarea data-val="true"
data-val-maxlength="The field Description must be a string or array type with a maximum length of
&#x27;1024&#x27;."
data-val-maxlength-max="1024"
data-val-minlength="The field Description must be a string or array type with a minimum length of
&#x27;5&#x27;."
data-val-minlength-min="5"
id="Description" name="Description">
</textarea>
<button type="submit">Test</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

Aplicación auxiliar de etiquetas Label


Genera el título de la etiqueta y el atributo for en un elemento de un nombre de expresión.
Alternativa de aplicación auxiliar HTML: Html.LabelFor .

Label Tag Helper proporciona las siguientes ventajas con respecto a un elemento HTML Label puro:
Obtendrá automáticamente el valor de la etiqueta descriptiva del atributo Display . El nombre para mostrar
que se busca puede cambiar con el tiempo y la combinación del atributo Display , y la aplicación auxiliar de
etiquetas Label aplicará el elemento Display en cualquier lugar donde se use.
El código fuente contiene menos marcado.
Permite establecer tipado fuerte con la propiedad de modelo.
Ejemplo:

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class SimpleViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }
}
}

@model SimpleViewModel

<form asp-controller="Demo" asp-action="RegisterLabel" method="post">


<label asp-for="Email"></label>
<input asp-for="Email" /> <br />
</form>

Se genera el siguiente código HTML para el elemento <label> :

<label for="Email">Email Address</label>

La aplicación auxiliar de etiquetas Label genera el valor de atributo for de "Email", que es el identificador asociado
al elemento <input> . Las aplicaciones auxiliares de etiqueta generan elementos id y for coherentes para que se
puedan asociar correctamente. El título de este ejemplo proviene del atributo Display . Si el modelo no contuviera
un atributo Display , el título sería el nombre de propiedad de la expresión.

Aplicaciones auxiliares de etiquetas de validación


Hay dos aplicaciones auxiliares de etiquetas de validación: Validation Message Tag Helper (que muestra un mensaje
de validación relativo a una única propiedad del modelo) y Validation Summary Tag Helper (que muestra un
resumen de los errores de validación). Input Tag Helper agrega atributos de validación del lado cliente HTML5 a
los elementos de entrada en función de los atributos de anotación de datos de las clases del modelo. La validación
también se realiza en el lado servidor. La aplicación auxiliar de etiquetas de validación muestra estos mensajes de
error cuando se produce un error de validación.
Aplicación auxiliar de etiquetas de mensaje de validación
Agrega el atributo HTML5 data-valmsg-for="property" al elemento span, que adjunta los mensajes de error
de validación en el campo de entrada de la propiedad de modelo especificada. Cuando se produce un error
de validación en el lado cliente, jQuery muestra el mensaje de error en el elemento <span> .
La validación también tiene lugar en el lado servidor. Puede que JavaScript esté deshabilitado en los clientes,
mientras que hay algunas validaciones que solo se pueden realizar en el lado servidor.
Alternativa de aplicación auxiliar HTML: Html.ValidationMessageFor .

Validation Message Tag Helper se usa con el atributo asp-validation-for en un elemento HTML span.

<span asp-validation-for="Email"></span>

La aplicación auxiliar de etiquetas de mensaje de validación generará el siguiente código HTML:

<span class="field-validation-valid"
data-valmsg-for="Email"
data-valmsg-replace="true"></span>

Validation Message Tag Helper suele usarse por lo general después de una aplicación auxiliar de etiquetas Input
en la misma propiedad. Gracias a esto, se mostrarán todos los mensajes de error de validación cerca de la entrada
que produjo el error.

NOTE
Debe tener una vista con las referencias de script de JavaScript y de jQuery adecuadas para la validación del lado cliente. Para
más información, vea Introduction to model validation in ASP.NET Core MVC (Introducción a la validación de modelos en
ASP.NET Core MVC).

Cuando se produce un error de validación del lado servidor (por ejemplo, porque haya una validación del lado
servidor personalizada o porque la validación del lado cliente esté deshabilitada), MVC pone ese mensaje de error
como cuerpo del elemento <span> .

<span class="field-validation-error" data-valmsg-for="Email"


data-valmsg-replace="true">
The Email Address field is required.
</span>

Aplicación auxiliar de etiquetas de resumen de validación


Tiene como destino todos los elementos <div> con el atributo asp-validation-summary .
Alternativa de aplicación auxiliar HTML: @Html.ValidationSummary .

Validation Summary Tag Helper se usa para mostrar un resumen de los mensajes de validación. El valor de atributo
asp-validation-summary puede ser cualquiera de los siguientes:

ASP-VALIDATION-SUMMARY MENSAJES DE VALIDACIÓN QUE SE MUESTRAN

ValidationSummary.All Nivel de modelo y de propiedad

ValidationSummary.ModelOnly Modelo

ValidationSummary.None Ninguna

Ejemplo
En el siguiente ejemplo, el modelo de datos se complementa con atributos DataAnnotation , lo que genera mensajes
de error de validación sobre el elemento <input> . Cuando se produce un error de validación, la aplicación auxiliar
de etiquetas de validación muestra el mensaje de error:

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }

[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
}

@model RegisterViewModel

<form asp-controller="Demo" asp-action="RegisterValidation" method="post">


<div asp-validation-summary="ModelOnly"></div>
Email: <input asp-for="Email" /> <br />
<span asp-validation-for="Email"></span><br />
Password: <input asp-for="Password" /><br />
<span asp-validation-for="Password"></span><br />
<button type="submit">Register</button>
</form>

El código HTML generado (cuando el modelo es válido) es este:


<form action="/DemoReg/Register" method="post">
<div class="validation-summary-valid" data-valmsg-summary="true">
<ul><li style="display:none"></li></ul></div>
Email: <input name="Email" id="Email" type="email" value=""
data-val-required="The Email field is required."
data-val-email="The Email field is not a valid email address."
data-val="true"> <br>
<span class="field-validation-valid" data-valmsg-replace="true"
data-valmsg-for="Email"></span><br>
Password: <input name="Password" id="Password" type="password"
data-val-required="The Password field is required." data-val="true"><br>
<span class="field-validation-valid" data-valmsg-replace="true"
data-valmsg-for="Password"></span><br>
<button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

Aplicación auxiliar de etiquetas de selección (Select)


Genera el elemento select y el elemento asociado option de las propiedades del modelo.
Tiene Html.DropDownListFor y Html.ListBoxFor como alternativa de aplicación auxiliar HTML.
El elemento asp-for de Select Tag Helper especifica el nombre de la propiedad de modelo del elemento select,
mientras que asp-items especifica los elementos option. Por ejemplo:

<select asp-for="Country" asp-items="Model.Countries"></select>

Ejemplo:

using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace FormsTagHelper.ViewModels
{
public class CountryViewModel
{
public string Country { get; set; }

public List<SelectListItem> Countries { get; } = new List<SelectListItem>


{
new SelectListItem { Value = "MX", Text = "Mexico" },
new SelectListItem { Value = "CA", Text = "Canada" },
new SelectListItem { Value = "US", Text = "USA" },
};
}
}

El método Index inicializa CountryViewModel , establece el país seleccionado y lo pasa a la vista Index .

public IActionResult IndexOption(int id)


{
var model = new CountryViewModel();
model.Country = "CA";
return View(model);
}

El método HTTP POST Index muestra la selección:


[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Index(CountryViewModel model)
{
if (ModelState.IsValid)
{
var msg = model.Country + " selected";
return RedirectToAction("IndexSuccess", new { message = msg});
}

// If we got this far, something failed; redisplay form.


return View(model);
}

La vista Index :

@model CountryViewModel

<form asp-controller="Home" asp-action="Index" method="post">


<select asp-for="Country" asp-items="Model.Countries"></select>
<br /><button type="submit">Register</button>
</form>

Genera el siguiente código HTML (con la selección "CA"):

<form method="post" action="/">


<select id="Country" name="Country">
<option value="MX">Mexico</option>
<option selected="selected" value="CA">Canada</option>
<option value="US">USA</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

NOTE
No se recomienda usar ViewBag o ViewData con la aplicación auxiliar de etiquetas Select. Un modelo de vista es más eficaz
a la hora de proporcionar metadatos MVC y suele ser menos problemático.

El valor de atributo asp-for es un caso especial y no necesita un prefijo Model , mientras que los otros atributos de
aplicación auxiliar de etiquetas sí (como asp-items ).

<select asp-for="Country" asp-items="Model.Countries"></select>

Enlace con enum


A menudo, conviene usar <select> con una propiedad enum y generar los elementos SelectListItem a partir de
valores enum .
Ejemplo:
public class CountryEnumViewModel
{
public CountryEnum EnumCountry { get; set; }
}
}

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public enum CountryEnum
{
[Display(Name = "United Mexican States")]
Mexico,
[Display(Name = "United States of America")]
USA,
Canada,
France,
Germany,
Spain
}
}

El método GetEnumSelectList genera un objeto SelectList para una enumeración.

@model CountryEnumViewModel

<form asp-controller="Home" asp-action="IndexEnum" method="post">


<select asp-for="EnumCountry"
asp-items="Html.GetEnumSelectList<CountryEnum>()"> >
</select>
<br /><button type="submit">Register</button>
</form>

Puede complementar la lista de enumeradores con el atributo Display para obtener una interfaz de usuario más
completa:

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public enum CountryEnum
{
[Display(Name = "United Mexican States")]
Mexico,
[Display(Name = "United States of America")]
USA,
Canada,
France,
Germany,
Spain
}
}

Se genera el siguiente código HTML:


<form method="post" action="/Home/IndexEnum">
<select data-val="true" data-val-required="The EnumCountry field is required."
id="EnumCountry" name="EnumCountry">
<option value="0">United Mexican States</option>
<option value="1">United States of America</option>
<option value="2">Canada</option>
<option value="3">France</option>
<option value="4">Germany</option>
<option selected="selected" value="5">Spain</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

Agrupamiento de opciones
El elemento HTML <optgroup> se genera cuando el modelo de vista contiene uno o varios objetos
SelectListGroup .

CountryViewModelGroup agrupa los elementos SelectListItem en los grupos "North America" y "Europe":
public class CountryViewModelGroup
{
public CountryViewModelGroup()
{
var NorthAmericaGroup = new SelectListGroup { Name = "North America" };
var EuropeGroup = new SelectListGroup { Name = "Europe" };

Countries = new List<SelectListItem>


{
new SelectListItem
{
Value = "MEX",
Text = "Mexico",
Group = NorthAmericaGroup
},
new SelectListItem
{
Value = "CAN",
Text = "Canada",
Group = NorthAmericaGroup
},
new SelectListItem
{
Value = "US",
Text = "USA",
Group = NorthAmericaGroup
},
new SelectListItem
{
Value = "FR",
Text = "France",
Group = EuropeGroup
},
new SelectListItem
{
Value = "ES",
Text = "Spain",
Group = EuropeGroup
},
new SelectListItem
{
Value = "DE",
Text = "Germany",
Group = EuropeGroup
}
};
}

public string Country { get; set; }

public List<SelectListItem> Countries { get; }

Aquí mostramos los dos grupos:


El código HTML generado:

<form method="post" action="/Home/IndexGroup">


<select id="Country" name="Country">
<optgroup label="North America">
<option value="MEX">Mexico</option>
<option value="CAN">Canada</option>
<option value="US">USA</option>
</optgroup>
<optgroup label="Europe">
<option value="FR">France</option>
<option value="ES">Spain</option>
<option value="DE">Germany</option>
</optgroup>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

Selección múltiple
La aplicación auxiliar de etiquetas Select generará automáticamente el atributo multiple = "multiple" si la propiedad
especificada en el atributo asp-for es IEnumerable . Por ejemplo, si tenemos el siguiente modelo:

using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace FormsTagHelper.ViewModels
{
public class CountryViewModelIEnumerable
{
public IEnumerable<string> CountryCodes { get; set; }

public List<SelectListItem> Countries { get; } = new List<SelectListItem>


{
new SelectListItem { Value = "MX", Text = "Mexico" },
new SelectListItem { Value = "CA", Text = "Canada" },
new SelectListItem { Value = "US", Text = "USA" },
new SelectListItem { Value = "FR", Text = "France" },
new SelectListItem { Value = "ES", Text = "Spain" },
new SelectListItem { Value = "DE", Text = "Germany"}
};
}
}

Con la siguiente vista:


@model CountryViewModelIEnumerable

<form asp-controller="Home" asp-action="IndexMultiSelect" method="post">


<select asp-for="CountryCodes" asp-items="Model.Countries"></select>
<br /><button type="submit">Register</button>
</form>

Se genera el siguiente código HTML:

<form method="post" action="/Home/IndexMultiSelect">


<select id="CountryCodes"
multiple="multiple"
name="CountryCodes"><option value="MX">Mexico</option>
<option value="CA">Canada</option>
<option value="US">USA</option>
<option value="FR">France</option>
<option value="ES">Spain</option>
<option value="DE">Germany</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

Sin selección
Si ve que usa la opción "sin especificar" en varias páginas, puede crear una plantilla para no tener que repetir el
código HTML:

@model CountryViewModel

<form asp-controller="Home" asp-action="IndexEmpty" method="post">


@Html.EditorForModel()
<br /><button type="submit">Register</button>
</form>

La plantilla Views/Shared/EditorTemplates/CountryViewModel.cshtml:

@model CountryViewModel

<select asp-for="Country" asp-items="Model.Countries">


<option value="">--none--</option>
</select>

La posibilidad de agregar elementos HTML <option> no se limita exclusivamente a los casos en los que no se
seleccionada nada. Por ejemplo, el método de acción y vista siguientes generarán un código HTML similar al
código anterior:

public IActionResult IndexOption(int id)


{
var model = new CountryViewModel();
model.Country = "CA";
return View(model);
}
@model CountryViewModel

<form asp-controller="Home" asp-action="IndexEmpty" method="post">


<select asp-for="Country">
<option value="">&lt;none&gt;</option>
<option value="MX">Mexico</option>
<option value="CA">Canada</option>
<option value="US">USA</option>
</select>
<br /><button type="submit">Register</button>
</form>

Se seleccionará el elemento <option> correcto (que contenga el atributo selected="selected" ) en función del valor
real de Country .

<form method="post" action="/Home/IndexEmpty">


<select id="Country" name="Country">
<option value="">&lt;none&gt;</option>
<option value="MX">Mexico</option>
<option value="CA" selected="selected">Canada</option>
<option value="US">USA</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

Recursos adicionales
Aplicaciones auxiliares de etiquetas
HTML Form element (Elemento HTML Form)
Request Verification Token (Token de comprobación de solicitudes)
Enlace de modelos
Introduction to model validation in ASP.NET Core MVC (Introducción a la validación de modelos en ASP.NET
Core MVC )
IAttributeAdapter Interface (Interfaz IAttributeAdapter)
Fragmentos de código de este documento
Aplicaciones auxiliares de etiquetas en formularios de
ASP.NET Core
19/06/2018 • 29 minutes to read • Edit Online

Por Rick Anderson, Dave Paquette y Jerrie Pelser


En este documento se explica cómo trabajar con formularios y se detallan los elementos HTML que se usan
habitualmente en un formulario. El elemento HTML Form proporciona el mecanismo principal que las aplicaciones
web usan a la hora de devolver datos al servidor. La mayor parte de este documento se centra en describir las
aplicaciones auxiliares de etiquetas y cómo pueden servir para crear formularios HTML eficaces de manera
productiva. Se recomienda leer Introduction to Tag Helpers (Introducción a las aplicaciones auxiliares de etiquetas)
antes de este documento.
En muchos casos, las aplicaciones auxiliares HTML proporcionan un método alternativo a una aplicación auxiliar de
etiquetas específica, pero es importante tener en cuenta que las aplicaciones auxiliares de etiquetas no reemplazan
a las aplicaciones auxiliares HTML y que, de igual modo, no hay una aplicación auxiliar de etiqueta para cada
aplicación auxiliar HTML. Si existe una alternativa de aplicación auxiliar HTML, se mencionará aquí.

Aplicación auxiliar de etiquetas de formulario (Form)


La aplicación auxiliar de etiquetas Form hace lo siguiente:
Genera el valor de atributo action del elemento HTML <FORM> de una acción de controlador MVC o ruta
con nombre.
Genera un token comprobación de solicitudes oculto que impide que se falsifiquen solicitudes entre sitios
(cuando se usa con el atributo [ValidateAntiForgeryToken] en el método de acción HTTP Post).
Proporciona el atributo asp-route-<Parameter Name> , donde <Parameter Name> se agrega a los valores de
ruta. Los parámetros routeValues de Html.BeginForm y Html.BeginRouteForm proporcionan una
funcionalidad similar.
Tiene Html.BeginForm y Html.BeginRouteForm como alternativa de aplicación auxiliar HTML.
Ejemplo:

<form asp-controller="Demo" asp-action="Register" method="post">


<!-- Input and Submit elements -->
</form>

La aplicación auxiliar de etiquetas Form anterior genera el siguiente HTML:

<form method="post" action="/Demo/Register">


<!-- Input and Submit elements -->
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

El tiempo de ejecución MVC genera el valor de atributo action de los atributos de aplicación auxiliar de etiquetas
Form asp-controller y asp-action . La aplicación auxiliar de etiquetas Form genera también un token de
comprobación de solicitudes oculto que impide que se falsifiquen solicitudes entre sitios (cuando se usa con el
atributo [ValidateAntiForgeryToken] en el método de acción HTTP Post). Proteger un elemento HTML Form puro
de la falsificación de solicitudes entre sitios no es tarea fácil, y la aplicación auxiliar de etiquetas Form presta este
servicio.
Uso de una ruta con nombre
El atributo de aplicación auxiliar de etiquetas asp-route puede generar también el marcado del atributo HTML
action . Una aplicación con una ruta denominada register podría usar el siguiente marcado para la página de
registro:

<form asp-route="register" method="post">


<!-- Input and Submit elements -->
</form>

Muchas de las vistas de la carpeta Views/Account (que se genera cuando se crea una aplicación web con Cuentas
de usuario individuales) contienen el atributo asp-route-returnurl:

<form asp-controller="Account" asp-action="Login"


asp-route-returnurl="@ViewData["ReturnUrl"]"
method="post" class="form-horizontal" role="form">

NOTE
Con las plantillas integradas, returnUrl se rellena automáticamente solo cuando alguien intenta obtener acceso a un
recurso autorizado, pero no se ha autenticado o no tiene autorización. Si se intenta realizar un acceso no autorizado, el
middleware de seguridad redirige a la página de inicio de sesión con returnUrl configurado.

Aplicación auxiliar de etiquetas de entrada (Input)


La aplicación auxiliar de etiquetas Input enlaza un elemento HTML <input> a una expresión de modelo en la vista
de Razor.
Sintaxis:

<input asp-for="<Expression Name>" />

La aplicación auxiliar de etiquetas Input hace lo siguiente:


Genera los atributos HTML y name del nombre de expresión especificado en el atributo asp-for .
id
asp-for="Property1.Property2" es equivalente a m => m.Property1.Property2 . El nombre de una expresión es
lo que se usa para el valor de atributo asp-for . Vea la sección Nombres de expresión para obtener más
información.
Establece el valor de atributo HTML type según los atributos de tipo de modelo y de anotación de datos
aplicados a la propiedad de modelo.
No sobrescribirá el valor de atributo HTML type cuando se especifique uno.
Genera atributos de validación HTML5 a partir de los atributos de anotación de datos aplicados a las
propiedades de modelo.
Tiene características de aplicación auxiliar HTML que se superponen a Html.TextBoxFor y Html.EditorFor .
Vea la sección Alternativas de aplicación auxiliar HTML a la aplicación auxiliar de etiquetas Input
para obtener más información.
Permite establecer tipado fuerte. Si el nombre de la propiedad cambia y no se actualiza la aplicación auxiliar
de etiquetas, aparecerá un error similar al siguiente:

An error occurred during the compilation of a resource required to process


this request. Please review the following specific error details and modify
your source code appropriately.

Type expected
'RegisterViewModel' does not contain a definition for 'Email' and no
extension method 'Email' accepting a first argument of type 'RegisterViewModel'
could be found (are you missing a using directive or an assembly reference?)

La aplicación auxiliar de etiquetas Input establece el atributo HTML type en función del tipo .NET. En la siguiente
tabla se enumeran algunos tipos .NET habituales y el tipo HTML generado correspondiente (no incluimos aquí
todos los tipos .NET).

TIPO DE .NET TIPO DE ENTRADA

Bool type=”checkbox”

String type=”text”

DateTime type=”datetime”

Byte type=”number”

Valor int. type=”number”

Single, Double type=”number”

En la siguiente tabla se muestran algunos atributos de anotación de datos comunes que la aplicación auxiliar de
etiquetas Input asignará a tipos de entrada concretos (no incluimos aquí todos los atributo de validación):

ATRIBUTO TIPO DE ENTRADA

[EmailAddress] type=”email”

[Url] type=”url”

[HiddenInput] type=”hidden”

[Phone] type=”tel”

[DataType(DataType.Password)] type=”password”

[DataType(DataType.Date)] type=”date”

[DataType(DataType.Time)] type=”time”

Ejemplo:
using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }

[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
}

@model RegisterViewModel

<form asp-controller="Demo" asp-action="RegisterInput" method="post">


Email: <input asp-for="Email" /> <br />
Password: <input asp-for="Password" /><br />
<button type="submit">Register</button>
</form>

El código anterior genera el siguiente HTML:

<form method="post" action="/Demo/RegisterInput">


Email:
<input type="email" data-val="true"
data-val-email="The Email Address field is not a valid email address."
data-val-required="The Email Address field is required."
id="Email" name="Email" value="" /> <br>
Password:
<input type="password" data-val="true"
data-val-required="The Password field is required."
id="Password" name="Password" /><br>
<button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

Las anotaciones de datos que se aplican a las propiedades Email y Password generan metadatos en el modelo. La
aplicación auxiliar de etiquetas Input usa esos metadatos del modelo y genera atributos HTML5 data-val-* . Vea
Model Validation (Validación del modelo). Estos atributos describen los validadores que se van a adjuntar a los
campos de entrada, lo que proporciona HTML5 discreto y validación de jQuery. Los atributos discretos tienen el
formato data-val-rule="Error Message" , donde "rule" es el nombre de la regla de validación (como
data-val-required , data-val-email , data-val-maxlength , etc.). Si aparece un mensaje de error en el atributo, se
mostrará como el valor del atributo data-val-rule . También hay atributos con el formato
data-val-ruleName-argumentName="argumentValue" que aportan más información sobre la regla, por ejemplo,
data-val-maxlength-max="1024" .

Alternativas de aplicación auxiliar HTML a la aplicación auxiliar de etiquetas Input


Html.TextBox , Html.TextBoxFor , Html.Editor y Html.EditorFor tienen características que se superponen a la
aplicación auxiliar de etiquetas Input. La aplicación auxiliar de etiquetas Input establecerá automáticamente el
atributo type , cosa que no ocurrirá con Html.TextBox ni Html.TextBoxFor . Html.Editor y Html.EditorFor
controlan colecciones, objetos complejos y plantillas, pero la aplicación auxiliar de etiquetas Input no. La aplicación
auxiliar de etiquetas Input, Html.EditorFor y Html.TextBoxFor están fuertemente tipados (usan expresiones
lambda), pero Html.TextBox y Html.Editor no (usan nombres de expresión).
HtmlAttributes
@Html.Editor() y @Html.EditorFor() usan una entrada ViewDataDictionary especial denominada htmlAttributes
al ejecutar sus plantillas predeterminadas. Si lo desea, este comportamiento se puede enriquecer con parámetros
additionalViewData . En la clave "htmlAttributes" se distingue entre mayúsculas y minúsculas. La clave
"htmlAttributes" se controla de forma similar al objeto htmlAttributes pasado a aplicaciones auxiliares de etiqueta
Input como @Html.TextBox() .

@Html.EditorFor(model => model.YourProperty,


new { htmlAttributes = new { @class="myCssClass", style="Width:100px" } })

Nombres de expresión
El valor del atributo asp-for es una ModelExpression y la parte de la derecha de una expresión lambda. Por tanto,
asp-for="Property1" se convierte en m => m.Property1 en el código generado, motivo por el que no es necesario
incluir el prefijo Model . Puede usar el carácter "@" para iniciar una expresión insertada y moverla antes de `m.`:
</span><span class="sxs-lookup">You can use the "@" character to start an inline expression and move before
the m. :

@{
var joe = "Joe";
}
<input asp-for="@joe" />

Se genera el siguiente HTML:

<input type="text" id="joe" name="joe" value="Joe" />

Con las propiedades de colección, asp-for="CollectionProperty[23].Member" genera el mismo nombre que


asp-for="CollectionProperty[i].Member" si i tiene el valor 23 .
Cuando ASP.NET Core MVC calcula el valor de ModelExpression , inspecciona varios orígenes, ModelState incluido.
Fíjese en <input type="text" asp-for="@Name" /> . El atributo value calculado es el primer valor distinto de null de:
La entrada ModelState con la clave "Name".
El resultado de la expresión Model.Name .
Navegar a las propiedades secundarias
También se puede navegar a las propiedades secundarias a través de la ruta de acceso de propiedades del modelo
de vista. Pensemos en una clase de modelo más compleja que contiene una propiedad secundaria Address .

public class AddressViewModel


{
public string AddressLine1 { get; set; }
}
public class RegisterAddressViewModel
{
public string Email { get; set; }

[DataType(DataType.Password)]
public string Password { get; set; }

public AddressViewModel Address { get; set; }


}

En la vista, enlazamos a Address.AddressLine1 :

@model RegisterAddressViewModel

<form asp-controller="Demo" asp-action="RegisterAddress" method="post">


Email: <input asp-for="Email" /> <br />
Password: <input asp-for="Password" /><br />
Address: <input asp-for="Address.AddressLine1" /><br />
<button type="submit">Register</button>
</form>

Se genera el siguiente código HTML para Address.AddressLine1 :

<input type="text" id="Address_AddressLine1" name="Address.AddressLine1" value="" />

Colecciones y nombres de expresión


Como ejemplo, un modelo que contiene una matriz de Colors :

public class Person


{
public List<string> Colors { get; set; }

public int Age { get; set; }


}

El método de acción:

public IActionResult Edit(int id, int colorIndex)


{
ViewData["Index"] = colorIndex;
return View(GetPerson(id));
}

El siguiente código de Razor muestra cómo se tiene acceso a un elemento Color concreto:

@model Person
@{
var index = (int)ViewData["index"];
}

<form asp-controller="ToDo" asp-action="Edit" method="post">


@Html.EditorFor(m => m.Colors[index])
<label asp-for="Age"></label>
<input asp-for="Age" /><br />
<button type="submit">Post</button>
</form>
La plantilla Views/Shared/EditorTemplates/String.cshtml:

@model string

<label asp-for="@Model"></label>
<input asp-for="@Model" /> <br />

Ejemplo en el que se usa List<T> :

public class ToDoItem


{
public string Name { get; set; }

public bool IsDone { get; set; }


}

El siguiente código de Razor muestra cómo iterar por una colección:

@model List<ToDoItem>

<form asp-controller="ToDo" asp-action="Edit" method="post">


<table>
<tr> <th>Name</th> <th>Is Done</th> </tr>

@for (int i = 0; i < Model.Count; i++)


{
<tr>
@Html.EditorFor(model => model[i])
</tr>
}

</table>
<button type="submit">Save</button>
</form>

La plantilla Views/Shared/EditorTemplates/ToDoItem.cshtml:

@model ToDoItem

<td>
<label asp-for="@Model.Name"></label>
@Html.DisplayFor(model => model.Name)
</td>
<td>
<input asp-for="@Model.IsDone" />
</td>

@*
This template replaces the following Razor which evaluates the indexer three times.
<td>
<label asp-for="@Model[i].Name"></label>
@Html.DisplayFor(model => model[i].Name)
</td>
<td>
<input asp-for="@Model[i].IsDone" />
</td>
*@
NOTE
Use siempre for (y no foreach ) para iterar por una lista. Evaluar un indizador en una expresión de LINQ puede ser
costoso, con lo cual esa posibilidad hay que reducirla al mínimo.

NOTE
El código de ejemplo comentado anterior muestra cómo reemplazaríamos la expresión lambda por el operador @ para tener
acceso a cada elemento ToDoItem de la lista.

Aplicación auxiliar de etiquetas de área de texto (Textarea)


La aplicación auxiliar de etiquetas Textarea Tag Helper es similar a la aplicación auxiliar de etiquetas Input.
Genera los atributos id y name , y los atributos de validación de datos del modelo de un elemento
<textarea>.
Permite establecer tipado fuerte.
Alternativa de aplicación auxiliar HTML: Html.TextAreaFor .

Ejemplo:

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class DescriptionViewModel
{
[MinLength(5)]
[MaxLength(1024)]
public string Description { get; set; }
}
}

@model DescriptionViewModel

<form asp-controller="Demo" asp-action="RegisterTextArea" method="post">


<textarea asp-for="Description"></textarea>
<button type="submit">Test</button>
</form>

Se genera el siguiente código HTML:


<form method="post" action="/Demo/RegisterTextArea">
<textarea data-val="true"
data-val-maxlength="The field Description must be a string or array type with a maximum length of
&#x27;1024&#x27;."
data-val-maxlength-max="1024"
data-val-minlength="The field Description must be a string or array type with a minimum length of
&#x27;5&#x27;."
data-val-minlength-min="5"
id="Description" name="Description">
</textarea>
<button type="submit">Test</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

Aplicación auxiliar de etiquetas Label


Genera el título de la etiqueta y el atributo for en un elemento de un nombre de expresión.
Alternativa de aplicación auxiliar HTML: Html.LabelFor .

Label Tag Helper proporciona las siguientes ventajas con respecto a un elemento HTML Label puro:
Obtendrá automáticamente el valor de la etiqueta descriptiva del atributo Display . El nombre para mostrar
que se busca puede cambiar con el tiempo y la combinación del atributo Display , y la aplicación auxiliar de
etiquetas Label aplicará el elemento Display en cualquier lugar donde se use.
El código fuente contiene menos marcado.
Permite establecer tipado fuerte con la propiedad de modelo.
Ejemplo:

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class SimpleViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }
}
}

@model SimpleViewModel

<form asp-controller="Demo" asp-action="RegisterLabel" method="post">


<label asp-for="Email"></label>
<input asp-for="Email" /> <br />
</form>

Se genera el siguiente código HTML para el elemento <label> :

<label for="Email">Email Address</label>

La aplicación auxiliar de etiquetas Label genera el valor de atributo for de "Email", que es el identificador asociado
al elemento <input> . Las aplicaciones auxiliares de etiqueta generan elementos id y for coherentes para que se
puedan asociar correctamente. El título de este ejemplo proviene del atributo Display . Si el modelo no contuviera
un atributo Display , el título sería el nombre de propiedad de la expresión.

Aplicaciones auxiliares de etiquetas de validación


Hay dos aplicaciones auxiliares de etiquetas de validación: Validation Message Tag Helper (que muestra un mensaje
de validación relativo a una única propiedad del modelo) y Validation Summary Tag Helper (que muestra un
resumen de los errores de validación). Input Tag Helper agrega atributos de validación del lado cliente HTML5 a
los elementos de entrada en función de los atributos de anotación de datos de las clases del modelo. La validación
también se realiza en el lado servidor. La aplicación auxiliar de etiquetas de validación muestra estos mensajes de
error cuando se produce un error de validación.
Aplicación auxiliar de etiquetas de mensaje de validación
Agrega el atributo HTML5 data-valmsg-for="property" al elemento span, que adjunta los mensajes de error
de validación en el campo de entrada de la propiedad de modelo especificada. Cuando se produce un error
de validación en el lado cliente, jQuery muestra el mensaje de error en el elemento <span> .
La validación también tiene lugar en el lado servidor. Puede que JavaScript esté deshabilitado en los clientes,
mientras que hay algunas validaciones que solo se pueden realizar en el lado servidor.
Alternativa de aplicación auxiliar HTML: Html.ValidationMessageFor .

Validation Message Tag Helper se usa con el atributo asp-validation-for en un elemento HTML span.

<span asp-validation-for="Email"></span>

La aplicación auxiliar de etiquetas de mensaje de validación generará el siguiente código HTML:

<span class="field-validation-valid"
data-valmsg-for="Email"
data-valmsg-replace="true"></span>

Validation Message Tag Helper suele usarse por lo general después de una aplicación auxiliar de etiquetas Input
en la misma propiedad. Gracias a esto, se mostrarán todos los mensajes de error de validación cerca de la entrada
que produjo el error.

NOTE
Debe tener una vista con las referencias de script de JavaScript y de jQuery adecuadas para la validación del lado cliente. Para
más información, vea Introduction to model validation in ASP.NET Core MVC (Introducción a la validación de modelos en
ASP.NET Core MVC).

Cuando se produce un error de validación del lado servidor (por ejemplo, porque haya una validación del lado
servidor personalizada o porque la validación del lado cliente esté deshabilitada), MVC pone ese mensaje de error
como cuerpo del elemento <span> .

<span class="field-validation-error" data-valmsg-for="Email"


data-valmsg-replace="true">
The Email Address field is required.
</span>

Aplicación auxiliar de etiquetas de resumen de validación


Tiene como destino todos los elementos <div> con el atributo asp-validation-summary .
Alternativa de aplicación auxiliar HTML: @Html.ValidationSummary .

Validation Summary Tag Helper se usa para mostrar un resumen de los mensajes de validación. El valor de atributo
asp-validation-summary puede ser cualquiera de los siguientes:

ASP-VALIDATION-SUMMARY MENSAJES DE VALIDACIÓN QUE SE MUESTRAN

ValidationSummary.All Nivel de modelo y de propiedad

ValidationSummary.ModelOnly Modelo

ValidationSummary.None Ninguna

Ejemplo
En el siguiente ejemplo, el modelo de datos se complementa con atributos DataAnnotation , lo que genera mensajes
de error de validación sobre el elemento <input> . Cuando se produce un error de validación, la aplicación auxiliar
de etiquetas de validación muestra el mensaje de error:

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }

[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
}

@model RegisterViewModel

<form asp-controller="Demo" asp-action="RegisterValidation" method="post">


<div asp-validation-summary="ModelOnly"></div>
Email: <input asp-for="Email" /> <br />
<span asp-validation-for="Email"></span><br />
Password: <input asp-for="Password" /><br />
<span asp-validation-for="Password"></span><br />
<button type="submit">Register</button>
</form>

El código HTML generado (cuando el modelo es válido) es este:


<form action="/DemoReg/Register" method="post">
<div class="validation-summary-valid" data-valmsg-summary="true">
<ul><li style="display:none"></li></ul></div>
Email: <input name="Email" id="Email" type="email" value=""
data-val-required="The Email field is required."
data-val-email="The Email field is not a valid email address."
data-val="true"> <br>
<span class="field-validation-valid" data-valmsg-replace="true"
data-valmsg-for="Email"></span><br>
Password: <input name="Password" id="Password" type="password"
data-val-required="The Password field is required." data-val="true"><br>
<span class="field-validation-valid" data-valmsg-replace="true"
data-valmsg-for="Password"></span><br>
<button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

Aplicación auxiliar de etiquetas de selección (Select)


Genera el elemento select y el elemento asociado option de las propiedades del modelo.
Tiene Html.DropDownListFor y Html.ListBoxFor como alternativa de aplicación auxiliar HTML.
El elemento asp-for de Select Tag Helper especifica el nombre de la propiedad de modelo del elemento select,
mientras que asp-items especifica los elementos option. Por ejemplo:

<select asp-for="Country" asp-items="Model.Countries"></select>

Ejemplo:

using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace FormsTagHelper.ViewModels
{
public class CountryViewModel
{
public string Country { get; set; }

public List<SelectListItem> Countries { get; } = new List<SelectListItem>


{
new SelectListItem { Value = "MX", Text = "Mexico" },
new SelectListItem { Value = "CA", Text = "Canada" },
new SelectListItem { Value = "US", Text = "USA" },
};
}
}

El método Index inicializa CountryViewModel , establece el país seleccionado y lo pasa a la vista Index .

public IActionResult IndexOption(int id)


{
var model = new CountryViewModel();
model.Country = "CA";
return View(model);
}

El método HTTP POST Index muestra la selección:


[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Index(CountryViewModel model)
{
if (ModelState.IsValid)
{
var msg = model.Country + " selected";
return RedirectToAction("IndexSuccess", new { message = msg});
}

// If we got this far, something failed; redisplay form.


return View(model);
}

La vista Index :

@model CountryViewModel

<form asp-controller="Home" asp-action="Index" method="post">


<select asp-for="Country" asp-items="Model.Countries"></select>
<br /><button type="submit">Register</button>
</form>

Genera el siguiente código HTML (con la selección "CA"):

<form method="post" action="/">


<select id="Country" name="Country">
<option value="MX">Mexico</option>
<option selected="selected" value="CA">Canada</option>
<option value="US">USA</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

NOTE
No se recomienda usar ViewBag o ViewData con la aplicación auxiliar de etiquetas Select. Un modelo de vista es más eficaz
a la hora de proporcionar metadatos MVC y suele ser menos problemático.

El valor de atributo asp-for es un caso especial y no necesita un prefijo Model , mientras que los otros atributos de
aplicación auxiliar de etiquetas sí (como asp-items ).

<select asp-for="Country" asp-items="Model.Countries"></select>

Enlace con enum


A menudo, conviene usar <select> con una propiedad enum y generar los elementos SelectListItem a partir de
valores enum .
Ejemplo:
public class CountryEnumViewModel
{
public CountryEnum EnumCountry { get; set; }
}
}

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public enum CountryEnum
{
[Display(Name = "United Mexican States")]
Mexico,
[Display(Name = "United States of America")]
USA,
Canada,
France,
Germany,
Spain
}
}

El método GetEnumSelectList genera un objeto SelectList para una enumeración.

@model CountryEnumViewModel

<form asp-controller="Home" asp-action="IndexEnum" method="post">


<select asp-for="EnumCountry"
asp-items="Html.GetEnumSelectList<CountryEnum>()"> >
</select>
<br /><button type="submit">Register</button>
</form>

Puede complementar la lista de enumeradores con el atributo Display para obtener una interfaz de usuario más
completa:

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public enum CountryEnum
{
[Display(Name = "United Mexican States")]
Mexico,
[Display(Name = "United States of America")]
USA,
Canada,
France,
Germany,
Spain
}
}

Se genera el siguiente código HTML:


<form method="post" action="/Home/IndexEnum">
<select data-val="true" data-val-required="The EnumCountry field is required."
id="EnumCountry" name="EnumCountry">
<option value="0">United Mexican States</option>
<option value="1">United States of America</option>
<option value="2">Canada</option>
<option value="3">France</option>
<option value="4">Germany</option>
<option selected="selected" value="5">Spain</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

Agrupamiento de opciones
El elemento HTML <optgroup> se genera cuando el modelo de vista contiene uno o varios objetos
SelectListGroup .

CountryViewModelGroup agrupa los elementos SelectListItem en los grupos "North America" y "Europe":
public class CountryViewModelGroup
{
public CountryViewModelGroup()
{
var NorthAmericaGroup = new SelectListGroup { Name = "North America" };
var EuropeGroup = new SelectListGroup { Name = "Europe" };

Countries = new List<SelectListItem>


{
new SelectListItem
{
Value = "MEX",
Text = "Mexico",
Group = NorthAmericaGroup
},
new SelectListItem
{
Value = "CAN",
Text = "Canada",
Group = NorthAmericaGroup
},
new SelectListItem
{
Value = "US",
Text = "USA",
Group = NorthAmericaGroup
},
new SelectListItem
{
Value = "FR",
Text = "France",
Group = EuropeGroup
},
new SelectListItem
{
Value = "ES",
Text = "Spain",
Group = EuropeGroup
},
new SelectListItem
{
Value = "DE",
Text = "Germany",
Group = EuropeGroup
}
};
}

public string Country { get; set; }

public List<SelectListItem> Countries { get; }

Aquí mostramos los dos grupos:


El código HTML generado:

<form method="post" action="/Home/IndexGroup">


<select id="Country" name="Country">
<optgroup label="North America">
<option value="MEX">Mexico</option>
<option value="CAN">Canada</option>
<option value="US">USA</option>
</optgroup>
<optgroup label="Europe">
<option value="FR">France</option>
<option value="ES">Spain</option>
<option value="DE">Germany</option>
</optgroup>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

Selección múltiple
La aplicación auxiliar de etiquetas Select generará automáticamente el atributo multiple = "multiple" si la propiedad
especificada en el atributo asp-for es IEnumerable . Por ejemplo, si tenemos el siguiente modelo:

using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace FormsTagHelper.ViewModels
{
public class CountryViewModelIEnumerable
{
public IEnumerable<string> CountryCodes { get; set; }

public List<SelectListItem> Countries { get; } = new List<SelectListItem>


{
new SelectListItem { Value = "MX", Text = "Mexico" },
new SelectListItem { Value = "CA", Text = "Canada" },
new SelectListItem { Value = "US", Text = "USA" },
new SelectListItem { Value = "FR", Text = "France" },
new SelectListItem { Value = "ES", Text = "Spain" },
new SelectListItem { Value = "DE", Text = "Germany"}
};
}
}

Con la siguiente vista:


@model CountryViewModelIEnumerable

<form asp-controller="Home" asp-action="IndexMultiSelect" method="post">


<select asp-for="CountryCodes" asp-items="Model.Countries"></select>
<br /><button type="submit">Register</button>
</form>

Se genera el siguiente código HTML:

<form method="post" action="/Home/IndexMultiSelect">


<select id="CountryCodes"
multiple="multiple"
name="CountryCodes"><option value="MX">Mexico</option>
<option value="CA">Canada</option>
<option value="US">USA</option>
<option value="FR">France</option>
<option value="ES">Spain</option>
<option value="DE">Germany</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

Sin selección
Si ve que usa la opción "sin especificar" en varias páginas, puede crear una plantilla para no tener que repetir el
código HTML:

@model CountryViewModel

<form asp-controller="Home" asp-action="IndexEmpty" method="post">


@Html.EditorForModel()
<br /><button type="submit">Register</button>
</form>

La plantilla Views/Shared/EditorTemplates/CountryViewModel.cshtml:

@model CountryViewModel

<select asp-for="Country" asp-items="Model.Countries">


<option value="">--none--</option>
</select>

La posibilidad de agregar elementos HTML <option> no se limita exclusivamente a los casos en los que no se
seleccionada nada. Por ejemplo, el método de acción y vista siguientes generarán un código HTML similar al
código anterior:

public IActionResult IndexOption(int id)


{
var model = new CountryViewModel();
model.Country = "CA";
return View(model);
}
@model CountryViewModel

<form asp-controller="Home" asp-action="IndexEmpty" method="post">


<select asp-for="Country">
<option value="">&lt;none&gt;</option>
<option value="MX">Mexico</option>
<option value="CA">Canada</option>
<option value="US">USA</option>
</select>
<br /><button type="submit">Register</button>
</form>

Se seleccionará el elemento <option> correcto (que contenga el atributo selected="selected" ) en función del valor
real de Country .

<form method="post" action="/Home/IndexEmpty">


<select id="Country" name="Country">
<option value="">&lt;none&gt;</option>
<option value="MX">Mexico</option>
<option value="CA" selected="selected">Canada</option>
<option value="US">USA</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

Recursos adicionales
Aplicaciones auxiliares de etiquetas
HTML Form element (Elemento HTML Form)
Request Verification Token (Token de comprobación de solicitudes)
Enlace de modelos
Introduction to model validation in ASP.NET Core MVC (Introducción a la validación de modelos en ASP.NET
Core MVC )
IAttributeAdapter Interface (Interfaz IAttributeAdapter)
Fragmentos de código de este documento
Aplicación auxiliar de etiquetas parciales en ASP.NET
Core
25/06/2018 • 4 minutes to read • Edit Online

Por Scott Addie


Vea o descargue el código de ejemplo (cómo descargarlo)

Información general
La aplicación auxiliar de etiquetas parciales sirve para representar una vista parcial en las páginas de Razor y las
aplicaciones MVC. Tenga en cuenta lo siguiente:
Es necesario ASP.NET Core 2.1 o una versión posterior.
Es una alternativa a la sintaxis de la aplicación auxiliar HTML.
Presenta la vista parcial de forma asincrónica.
Las opciones de la aplicación auxiliar HTML para representar una vista parcial son estas:
@await Html.PartialAsync
@await Html.RenderPartialAsync
@Html.Partial
@Html.RenderPartial
En los ejemplos de todo este documento se usa el modelo Product:

namespace TagHelpersBuiltIn.Models
{
public class Product
{
public int Number { get; set; }

public string Name { get; set; }

public string Description { get; set; }


}
}

Ahora pasaremos a ver una relación de los atributos de la aplicación auxiliar de etiquetas parciales.

name
El atributo name es necesario. Señala el nombre o la ruta de acceso de la vista parcial que se va a representar.
Cuando se indica el nombre de una vista parcial, se inicia el proceso de detección de vista. Este proceso se omite
cuando se proporciona una ruta de acceso explícita.
En el siguiente marcado se usa una ruta de acceso explícita, lo que indica que _ProductPartial.cshtml debe
cargarse desde la carpeta Shared. Mediante el atributo for, se pasa un modelo a la vista parcial para el enlace.

<partial name="Shared/_ProductPartial.cshtml"
for="Product" />
for
El atributo for asigna una ModelExpression para que se evalúe según el modelo actual. ModelExpression deduce
la sintaxis de @Model. . Por ejemplo, se puede usar for="Product" en lugar de for="@Model.Product" . Este
comportamiento predeterminado de deducción queda invalidado si se usa el símbolo @ para definir una
expresión insertada. El atributo for no se puede usar con el atributo model.
El siguiente marcado carga _ProductPartial.cshtml:

<partial name="_ProductPartial"
for="Product" />

La vista parcial se enlaza a la propiedad Product del modelo de página asociado correspondiente:

using Microsoft.AspNetCore.Mvc.RazorPages;
using TagHelpersBuiltIn.Models;

namespace TagHelpersBuiltIn.Pages
{
public class ProductModel : PageModel
{
public Product Product { get; set; }

public void OnGet()


{
Product = new Product
{
Number = 1,
Name = "Test product",
Description = "This is a test product"
};
}
}
}

modelo
El atributo model asigna una instancia de modelo para pasarla a la vista parcial. El atributo model no se puede
usar con el atributo for.
En el siguiente código de marcado, se crea un objeto Product y se pasa al atributo model para el enlace:

<partial name="_ProductPartial"
model='new Product { Number = 1, Name = "Test product", Description = "This is a test" }' />

view-data
El atributo view-data asigna un ViewDataDictionary para pasarlo a la vista parcial. El siguiente marcado hace que
toda la colección ViewData esté accesible para la vista parcial:
@{
ViewData["IsNumberReadOnly"] = true;
}

<partial name="_ProductViewDataPartial"
for="Product"
view-data="@ViewData" />

En el código anterior, el valor de clave IsNumberReadOnly está establecido en true y se ha agregado a la colección
ViewData. Por tanto, ViewData["IsNumberReadOnly"] estará accesible dentro de la vista parcial siguiente:

@model TagHelpersBuiltIn.Models.Product

<div class="form-group">
<label asp-for="Number"></label>
@if ((bool)ViewData["IsNumberReadOnly"])
{
<input asp-for="Number" type="number" class="form-control" readonly />
}
else
{
<input asp-for="Number" type="number" class="form-control" />
}
</div>
<div class="form-group">
<label asp-for="Name"></label>
<input asp-for="Name" type="text" class="form-control" />
</div>
<div class="form-group">
<label asp-for="Description"></label>
<textarea asp-for="Description" rows="4" cols="50" class="form-control"></textarea>
</div>

En este ejemplo, el valor de ViewData["IsNumberReadOnly"] determina si el campo Number se muestra como de


solo lectura.

Recursos adicionales
Vistas parciales
Datos con establecimiento flexible de tipos (ViewData, atributo ViewData y ViewBag)
Aplicaciones auxiliares de etiquetas en formularios de
ASP.NET Core
19/06/2018 • 29 minutes to read • Edit Online

Por Rick Anderson, Dave Paquette y Jerrie Pelser


En este documento se explica cómo trabajar con formularios y se detallan los elementos HTML que se usan
habitualmente en un formulario. El elemento HTML Form proporciona el mecanismo principal que las aplicaciones
web usan a la hora de devolver datos al servidor. La mayor parte de este documento se centra en describir las
aplicaciones auxiliares de etiquetas y cómo pueden servir para crear formularios HTML eficaces de manera
productiva. Se recomienda leer Introduction to Tag Helpers (Introducción a las aplicaciones auxiliares de etiquetas)
antes de este documento.
En muchos casos, las aplicaciones auxiliares HTML proporcionan un método alternativo a una aplicación auxiliar de
etiquetas específica, pero es importante tener en cuenta que las aplicaciones auxiliares de etiquetas no reemplazan
a las aplicaciones auxiliares HTML y que, de igual modo, no hay una aplicación auxiliar de etiqueta para cada
aplicación auxiliar HTML. Si existe una alternativa de aplicación auxiliar HTML, se mencionará aquí.

Aplicación auxiliar de etiquetas de formulario (Form)


La aplicación auxiliar de etiquetas Form hace lo siguiente:
Genera el valor de atributo action del elemento HTML <FORM> de una acción de controlador MVC o ruta
con nombre.
Genera un token comprobación de solicitudes oculto que impide que se falsifiquen solicitudes entre sitios
(cuando se usa con el atributo [ValidateAntiForgeryToken] en el método de acción HTTP Post).
Proporciona el atributo asp-route-<Parameter Name> , donde <Parameter Name> se agrega a los valores de
ruta. Los parámetros routeValues de Html.BeginForm y Html.BeginRouteForm proporcionan una
funcionalidad similar.
Tiene Html.BeginForm y Html.BeginRouteForm como alternativa de aplicación auxiliar HTML.
Ejemplo:

<form asp-controller="Demo" asp-action="Register" method="post">


<!-- Input and Submit elements -->
</form>

La aplicación auxiliar de etiquetas Form anterior genera el siguiente HTML:

<form method="post" action="/Demo/Register">


<!-- Input and Submit elements -->
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

El tiempo de ejecución MVC genera el valor de atributo action de los atributos de aplicación auxiliar de etiquetas
Form asp-controller y asp-action . La aplicación auxiliar de etiquetas Form genera también un token de
comprobación de solicitudes oculto que impide que se falsifiquen solicitudes entre sitios (cuando se usa con el
atributo [ValidateAntiForgeryToken] en el método de acción HTTP Post). Proteger un elemento HTML Form puro
de la falsificación de solicitudes entre sitios no es tarea fácil, y la aplicación auxiliar de etiquetas Form presta este
servicio.
Uso de una ruta con nombre
El atributo de aplicación auxiliar de etiquetas asp-route puede generar también el marcado del atributo HTML
action . Una aplicación con una ruta denominada register podría usar el siguiente marcado para la página de
registro:

<form asp-route="register" method="post">


<!-- Input and Submit elements -->
</form>

Muchas de las vistas de la carpeta Views/Account (que se genera cuando se crea una aplicación web con Cuentas
de usuario individuales) contienen el atributo asp-route-returnurl:

<form asp-controller="Account" asp-action="Login"


asp-route-returnurl="@ViewData["ReturnUrl"]"
method="post" class="form-horizontal" role="form">

NOTE
Con las plantillas integradas, returnUrl se rellena automáticamente solo cuando alguien intenta obtener acceso a un
recurso autorizado, pero no se ha autenticado o no tiene autorización. Si se intenta realizar un acceso no autorizado, el
middleware de seguridad redirige a la página de inicio de sesión con returnUrl configurado.

Aplicación auxiliar de etiquetas de entrada (Input)


La aplicación auxiliar de etiquetas Input enlaza un elemento HTML <input> a una expresión de modelo en la vista
de Razor.
Sintaxis:

<input asp-for="<Expression Name>" />

La aplicación auxiliar de etiquetas Input hace lo siguiente:


Genera los atributos HTML y name del nombre de expresión especificado en el atributo asp-for .
id
asp-for="Property1.Property2" es equivalente a m => m.Property1.Property2 . El nombre de una expresión es
lo que se usa para el valor de atributo asp-for . Vea la sección Nombres de expresión para obtener más
información.
Establece el valor de atributo HTML type según los atributos de tipo de modelo y de anotación de datos
aplicados a la propiedad de modelo.
No sobrescribirá el valor de atributo HTML type cuando se especifique uno.
Genera atributos de validación HTML5 a partir de los atributos de anotación de datos aplicados a las
propiedades de modelo.
Tiene características de aplicación auxiliar HTML que se superponen a Html.TextBoxFor y Html.EditorFor .
Vea la sección Alternativas de aplicación auxiliar HTML a la aplicación auxiliar de etiquetas Input
para obtener más información.
Permite establecer tipado fuerte. Si el nombre de la propiedad cambia y no se actualiza la aplicación auxiliar
de etiquetas, aparecerá un error similar al siguiente:

An error occurred during the compilation of a resource required to process


this request. Please review the following specific error details and modify
your source code appropriately.

Type expected
'RegisterViewModel' does not contain a definition for 'Email' and no
extension method 'Email' accepting a first argument of type 'RegisterViewModel'
could be found (are you missing a using directive or an assembly reference?)

La aplicación auxiliar de etiquetas Input establece el atributo HTML type en función del tipo .NET. En la siguiente
tabla se enumeran algunos tipos .NET habituales y el tipo HTML generado correspondiente (no incluimos aquí
todos los tipos .NET).

TIPO DE .NET TIPO DE ENTRADA

Bool type=”checkbox”

String type=”text”

DateTime type=”datetime”

Byte type=”number”

Valor int. type=”number”

Single, Double type=”number”

En la siguiente tabla se muestran algunos atributos de anotación de datos comunes que la aplicación auxiliar de
etiquetas Input asignará a tipos de entrada concretos (no incluimos aquí todos los atributo de validación):

ATRIBUTO TIPO DE ENTRADA

[EmailAddress] type=”email”

[Url] type=”url”

[HiddenInput] type=”hidden”

[Phone] type=”tel”

[DataType(DataType.Password)] type=”password”

[DataType(DataType.Date)] type=”date”

[DataType(DataType.Time)] type=”time”

Ejemplo:
using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }

[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
}

@model RegisterViewModel

<form asp-controller="Demo" asp-action="RegisterInput" method="post">


Email: <input asp-for="Email" /> <br />
Password: <input asp-for="Password" /><br />
<button type="submit">Register</button>
</form>

El código anterior genera el siguiente HTML:

<form method="post" action="/Demo/RegisterInput">


Email:
<input type="email" data-val="true"
data-val-email="The Email Address field is not a valid email address."
data-val-required="The Email Address field is required."
id="Email" name="Email" value="" /> <br>
Password:
<input type="password" data-val="true"
data-val-required="The Password field is required."
id="Password" name="Password" /><br>
<button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

Las anotaciones de datos que se aplican a las propiedades Email y Password generan metadatos en el modelo. La
aplicación auxiliar de etiquetas Input usa esos metadatos del modelo y genera atributos HTML5 data-val-* . Vea
Model Validation (Validación del modelo). Estos atributos describen los validadores que se van a adjuntar a los
campos de entrada, lo que proporciona HTML5 discreto y validación de jQuery. Los atributos discretos tienen el
formato data-val-rule="Error Message" , donde "rule" es el nombre de la regla de validación (como
data-val-required , data-val-email , data-val-maxlength , etc.). Si aparece un mensaje de error en el atributo, se
mostrará como el valor del atributo data-val-rule . También hay atributos con el formato
data-val-ruleName-argumentName="argumentValue" que aportan más información sobre la regla, por ejemplo,
data-val-maxlength-max="1024" .

Alternativas de aplicación auxiliar HTML a la aplicación auxiliar de etiquetas Input


Html.TextBox , Html.TextBoxFor , Html.Editor y Html.EditorFor tienen características que se superponen a la
aplicación auxiliar de etiquetas Input. La aplicación auxiliar de etiquetas Input establecerá automáticamente el
atributo type , cosa que no ocurrirá con Html.TextBox ni Html.TextBoxFor . Html.Editor y Html.EditorFor
controlan colecciones, objetos complejos y plantillas, pero la aplicación auxiliar de etiquetas Input no. La aplicación
auxiliar de etiquetas Input, Html.EditorFor y Html.TextBoxFor están fuertemente tipados (usan expresiones
lambda), pero Html.TextBox y Html.Editor no (usan nombres de expresión).
HtmlAttributes
@Html.Editor() y @Html.EditorFor() usan una entrada ViewDataDictionary especial denominada htmlAttributes
al ejecutar sus plantillas predeterminadas. Si lo desea, este comportamiento se puede enriquecer con parámetros
additionalViewData . En la clave "htmlAttributes" se distingue entre mayúsculas y minúsculas. La clave
"htmlAttributes" se controla de forma similar al objeto htmlAttributes pasado a aplicaciones auxiliares de etiqueta
Input como @Html.TextBox() .

@Html.EditorFor(model => model.YourProperty,


new { htmlAttributes = new { @class="myCssClass", style="Width:100px" } })

Nombres de expresión
El valor del atributo asp-for es una ModelExpression y la parte de la derecha de una expresión lambda. Por tanto,
asp-for="Property1" se convierte en m => m.Property1 en el código generado, motivo por el que no es necesario
incluir el prefijo Model . Puede usar el carácter "@" para iniciar una expresión insertada y moverla antes de `m.`:
</span><span class="sxs-lookup">You can use the "@" character to start an inline expression and move before
the m. :

@{
var joe = "Joe";
}
<input asp-for="@joe" />

Se genera el siguiente HTML:

<input type="text" id="joe" name="joe" value="Joe" />

Con las propiedades de colección, asp-for="CollectionProperty[23].Member" genera el mismo nombre que


asp-for="CollectionProperty[i].Member" si i tiene el valor 23 .
Cuando ASP.NET Core MVC calcula el valor de ModelExpression , inspecciona varios orígenes, ModelState incluido.
Fíjese en <input type="text" asp-for="@Name" /> . El atributo value calculado es el primer valor distinto de null de:
La entrada ModelState con la clave "Name".
El resultado de la expresión Model.Name .
Navegar a las propiedades secundarias
También se puede navegar a las propiedades secundarias a través de la ruta de acceso de propiedades del modelo
de vista. Pensemos en una clase de modelo más compleja que contiene una propiedad secundaria Address .

public class AddressViewModel


{
public string AddressLine1 { get; set; }
}
public class RegisterAddressViewModel
{
public string Email { get; set; }

[DataType(DataType.Password)]
public string Password { get; set; }

public AddressViewModel Address { get; set; }


}

En la vista, enlazamos a Address.AddressLine1 :

@model RegisterAddressViewModel

<form asp-controller="Demo" asp-action="RegisterAddress" method="post">


Email: <input asp-for="Email" /> <br />
Password: <input asp-for="Password" /><br />
Address: <input asp-for="Address.AddressLine1" /><br />
<button type="submit">Register</button>
</form>

Se genera el siguiente código HTML para Address.AddressLine1 :

<input type="text" id="Address_AddressLine1" name="Address.AddressLine1" value="" />

Colecciones y nombres de expresión


Como ejemplo, un modelo que contiene una matriz de Colors :

public class Person


{
public List<string> Colors { get; set; }

public int Age { get; set; }


}

El método de acción:

public IActionResult Edit(int id, int colorIndex)


{
ViewData["Index"] = colorIndex;
return View(GetPerson(id));
}

El siguiente código de Razor muestra cómo se tiene acceso a un elemento Color concreto:

@model Person
@{
var index = (int)ViewData["index"];
}

<form asp-controller="ToDo" asp-action="Edit" method="post">


@Html.EditorFor(m => m.Colors[index])
<label asp-for="Age"></label>
<input asp-for="Age" /><br />
<button type="submit">Post</button>
</form>
La plantilla Views/Shared/EditorTemplates/String.cshtml:

@model string

<label asp-for="@Model"></label>
<input asp-for="@Model" /> <br />

Ejemplo en el que se usa List<T> :

public class ToDoItem


{
public string Name { get; set; }

public bool IsDone { get; set; }


}

El siguiente código de Razor muestra cómo iterar por una colección:

@model List<ToDoItem>

<form asp-controller="ToDo" asp-action="Edit" method="post">


<table>
<tr> <th>Name</th> <th>Is Done</th> </tr>

@for (int i = 0; i < Model.Count; i++)


{
<tr>
@Html.EditorFor(model => model[i])
</tr>
}

</table>
<button type="submit">Save</button>
</form>

La plantilla Views/Shared/EditorTemplates/ToDoItem.cshtml:

@model ToDoItem

<td>
<label asp-for="@Model.Name"></label>
@Html.DisplayFor(model => model.Name)
</td>
<td>
<input asp-for="@Model.IsDone" />
</td>

@*
This template replaces the following Razor which evaluates the indexer three times.
<td>
<label asp-for="@Model[i].Name"></label>
@Html.DisplayFor(model => model[i].Name)
</td>
<td>
<input asp-for="@Model[i].IsDone" />
</td>
*@
NOTE
Use siempre for (y no foreach ) para iterar por una lista. Evaluar un indizador en una expresión de LINQ puede ser
costoso, con lo cual esa posibilidad hay que reducirla al mínimo.

NOTE
El código de ejemplo comentado anterior muestra cómo reemplazaríamos la expresión lambda por el operador @ para tener
acceso a cada elemento ToDoItem de la lista.

Aplicación auxiliar de etiquetas de área de texto (Textarea)


La aplicación auxiliar de etiquetas Textarea Tag Helper es similar a la aplicación auxiliar de etiquetas Input.
Genera los atributos id y name , y los atributos de validación de datos del modelo de un elemento
<textarea>.
Permite establecer tipado fuerte.
Alternativa de aplicación auxiliar HTML: Html.TextAreaFor .

Ejemplo:

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class DescriptionViewModel
{
[MinLength(5)]
[MaxLength(1024)]
public string Description { get; set; }
}
}

@model DescriptionViewModel

<form asp-controller="Demo" asp-action="RegisterTextArea" method="post">


<textarea asp-for="Description"></textarea>
<button type="submit">Test</button>
</form>

Se genera el siguiente código HTML:


<form method="post" action="/Demo/RegisterTextArea">
<textarea data-val="true"
data-val-maxlength="The field Description must be a string or array type with a maximum length of
&#x27;1024&#x27;."
data-val-maxlength-max="1024"
data-val-minlength="The field Description must be a string or array type with a minimum length of
&#x27;5&#x27;."
data-val-minlength-min="5"
id="Description" name="Description">
</textarea>
<button type="submit">Test</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

Aplicación auxiliar de etiquetas Label


Genera el título de la etiqueta y el atributo for en un elemento de un nombre de expresión.
Alternativa de aplicación auxiliar HTML: Html.LabelFor .

Label Tag Helper proporciona las siguientes ventajas con respecto a un elemento HTML Label puro:
Obtendrá automáticamente el valor de la etiqueta descriptiva del atributo Display . El nombre para mostrar
que se busca puede cambiar con el tiempo y la combinación del atributo Display , y la aplicación auxiliar de
etiquetas Label aplicará el elemento Display en cualquier lugar donde se use.
El código fuente contiene menos marcado.
Permite establecer tipado fuerte con la propiedad de modelo.
Ejemplo:

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class SimpleViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }
}
}

@model SimpleViewModel

<form asp-controller="Demo" asp-action="RegisterLabel" method="post">


<label asp-for="Email"></label>
<input asp-for="Email" /> <br />
</form>

Se genera el siguiente código HTML para el elemento <label> :

<label for="Email">Email Address</label>

La aplicación auxiliar de etiquetas Label genera el valor de atributo for de "Email", que es el identificador asociado
al elemento <input> . Las aplicaciones auxiliares de etiqueta generan elementos id y for coherentes para que se
puedan asociar correctamente. El título de este ejemplo proviene del atributo Display . Si el modelo no contuviera
un atributo Display , el título sería el nombre de propiedad de la expresión.

Aplicaciones auxiliares de etiquetas de validación


Hay dos aplicaciones auxiliares de etiquetas de validación: Validation Message Tag Helper (que muestra un mensaje
de validación relativo a una única propiedad del modelo) y Validation Summary Tag Helper (que muestra un
resumen de los errores de validación). Input Tag Helper agrega atributos de validación del lado cliente HTML5 a
los elementos de entrada en función de los atributos de anotación de datos de las clases del modelo. La validación
también se realiza en el lado servidor. La aplicación auxiliar de etiquetas de validación muestra estos mensajes de
error cuando se produce un error de validación.
Aplicación auxiliar de etiquetas de mensaje de validación
Agrega el atributo HTML5 data-valmsg-for="property" al elemento span, que adjunta los mensajes de error
de validación en el campo de entrada de la propiedad de modelo especificada. Cuando se produce un error
de validación en el lado cliente, jQuery muestra el mensaje de error en el elemento <span> .
La validación también tiene lugar en el lado servidor. Puede que JavaScript esté deshabilitado en los clientes,
mientras que hay algunas validaciones que solo se pueden realizar en el lado servidor.
Alternativa de aplicación auxiliar HTML: Html.ValidationMessageFor .

Validation Message Tag Helper se usa con el atributo asp-validation-for en un elemento HTML span.

<span asp-validation-for="Email"></span>

La aplicación auxiliar de etiquetas de mensaje de validación generará el siguiente código HTML:

<span class="field-validation-valid"
data-valmsg-for="Email"
data-valmsg-replace="true"></span>

Validation Message Tag Helper suele usarse por lo general después de una aplicación auxiliar de etiquetas Input
en la misma propiedad. Gracias a esto, se mostrarán todos los mensajes de error de validación cerca de la entrada
que produjo el error.

NOTE
Debe tener una vista con las referencias de script de JavaScript y de jQuery adecuadas para la validación del lado cliente. Para
más información, vea Introduction to model validation in ASP.NET Core MVC (Introducción a la validación de modelos en
ASP.NET Core MVC).

Cuando se produce un error de validación del lado servidor (por ejemplo, porque haya una validación del lado
servidor personalizada o porque la validación del lado cliente esté deshabilitada), MVC pone ese mensaje de error
como cuerpo del elemento <span> .

<span class="field-validation-error" data-valmsg-for="Email"


data-valmsg-replace="true">
The Email Address field is required.
</span>

Aplicación auxiliar de etiquetas de resumen de validación


Tiene como destino todos los elementos <div> con el atributo asp-validation-summary .
Alternativa de aplicación auxiliar HTML: @Html.ValidationSummary .

Validation Summary Tag Helper se usa para mostrar un resumen de los mensajes de validación. El valor de atributo
asp-validation-summary puede ser cualquiera de los siguientes:

ASP-VALIDATION-SUMMARY MENSAJES DE VALIDACIÓN QUE SE MUESTRAN

ValidationSummary.All Nivel de modelo y de propiedad

ValidationSummary.ModelOnly Modelo

ValidationSummary.None Ninguna

Ejemplo
En el siguiente ejemplo, el modelo de datos se complementa con atributos DataAnnotation , lo que genera mensajes
de error de validación sobre el elemento <input> . Cuando se produce un error de validación, la aplicación auxiliar
de etiquetas de validación muestra el mensaje de error:

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }

[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
}

@model RegisterViewModel

<form asp-controller="Demo" asp-action="RegisterValidation" method="post">


<div asp-validation-summary="ModelOnly"></div>
Email: <input asp-for="Email" /> <br />
<span asp-validation-for="Email"></span><br />
Password: <input asp-for="Password" /><br />
<span asp-validation-for="Password"></span><br />
<button type="submit">Register</button>
</form>

El código HTML generado (cuando el modelo es válido) es este:


<form action="/DemoReg/Register" method="post">
<div class="validation-summary-valid" data-valmsg-summary="true">
<ul><li style="display:none"></li></ul></div>
Email: <input name="Email" id="Email" type="email" value=""
data-val-required="The Email field is required."
data-val-email="The Email field is not a valid email address."
data-val="true"> <br>
<span class="field-validation-valid" data-valmsg-replace="true"
data-valmsg-for="Email"></span><br>
Password: <input name="Password" id="Password" type="password"
data-val-required="The Password field is required." data-val="true"><br>
<span class="field-validation-valid" data-valmsg-replace="true"
data-valmsg-for="Password"></span><br>
<button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

Aplicación auxiliar de etiquetas de selección (Select)


Genera el elemento select y el elemento asociado option de las propiedades del modelo.
Tiene Html.DropDownListFor y Html.ListBoxFor como alternativa de aplicación auxiliar HTML.
El elemento asp-for de Select Tag Helper especifica el nombre de la propiedad de modelo del elemento select,
mientras que asp-items especifica los elementos option. Por ejemplo:

<select asp-for="Country" asp-items="Model.Countries"></select>

Ejemplo:

using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace FormsTagHelper.ViewModels
{
public class CountryViewModel
{
public string Country { get; set; }

public List<SelectListItem> Countries { get; } = new List<SelectListItem>


{
new SelectListItem { Value = "MX", Text = "Mexico" },
new SelectListItem { Value = "CA", Text = "Canada" },
new SelectListItem { Value = "US", Text = "USA" },
};
}
}

El método Index inicializa CountryViewModel , establece el país seleccionado y lo pasa a la vista Index .

public IActionResult IndexOption(int id)


{
var model = new CountryViewModel();
model.Country = "CA";
return View(model);
}

El método HTTP POST Index muestra la selección:


[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Index(CountryViewModel model)
{
if (ModelState.IsValid)
{
var msg = model.Country + " selected";
return RedirectToAction("IndexSuccess", new { message = msg});
}

// If we got this far, something failed; redisplay form.


return View(model);
}

La vista Index :

@model CountryViewModel

<form asp-controller="Home" asp-action="Index" method="post">


<select asp-for="Country" asp-items="Model.Countries"></select>
<br /><button type="submit">Register</button>
</form>

Genera el siguiente código HTML (con la selección "CA"):

<form method="post" action="/">


<select id="Country" name="Country">
<option value="MX">Mexico</option>
<option selected="selected" value="CA">Canada</option>
<option value="US">USA</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

NOTE
No se recomienda usar ViewBag o ViewData con la aplicación auxiliar de etiquetas Select. Un modelo de vista es más eficaz
a la hora de proporcionar metadatos MVC y suele ser menos problemático.

El valor de atributo asp-for es un caso especial y no necesita un prefijo Model , mientras que los otros atributos de
aplicación auxiliar de etiquetas sí (como asp-items ).

<select asp-for="Country" asp-items="Model.Countries"></select>

Enlace con enum


A menudo, conviene usar <select> con una propiedad enum y generar los elementos SelectListItem a partir de
valores enum .
Ejemplo:
public class CountryEnumViewModel
{
public CountryEnum EnumCountry { get; set; }
}
}

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public enum CountryEnum
{
[Display(Name = "United Mexican States")]
Mexico,
[Display(Name = "United States of America")]
USA,
Canada,
France,
Germany,
Spain
}
}

El método GetEnumSelectList genera un objeto SelectList para una enumeración.

@model CountryEnumViewModel

<form asp-controller="Home" asp-action="IndexEnum" method="post">


<select asp-for="EnumCountry"
asp-items="Html.GetEnumSelectList<CountryEnum>()"> >
</select>
<br /><button type="submit">Register</button>
</form>

Puede complementar la lista de enumeradores con el atributo Display para obtener una interfaz de usuario más
completa:

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public enum CountryEnum
{
[Display(Name = "United Mexican States")]
Mexico,
[Display(Name = "United States of America")]
USA,
Canada,
France,
Germany,
Spain
}
}

Se genera el siguiente código HTML:


<form method="post" action="/Home/IndexEnum">
<select data-val="true" data-val-required="The EnumCountry field is required."
id="EnumCountry" name="EnumCountry">
<option value="0">United Mexican States</option>
<option value="1">United States of America</option>
<option value="2">Canada</option>
<option value="3">France</option>
<option value="4">Germany</option>
<option selected="selected" value="5">Spain</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

Agrupamiento de opciones
El elemento HTML <optgroup> se genera cuando el modelo de vista contiene uno o varios objetos
SelectListGroup .

CountryViewModelGroup agrupa los elementos SelectListItem en los grupos "North America" y "Europe":
public class CountryViewModelGroup
{
public CountryViewModelGroup()
{
var NorthAmericaGroup = new SelectListGroup { Name = "North America" };
var EuropeGroup = new SelectListGroup { Name = "Europe" };

Countries = new List<SelectListItem>


{
new SelectListItem
{
Value = "MEX",
Text = "Mexico",
Group = NorthAmericaGroup
},
new SelectListItem
{
Value = "CAN",
Text = "Canada",
Group = NorthAmericaGroup
},
new SelectListItem
{
Value = "US",
Text = "USA",
Group = NorthAmericaGroup
},
new SelectListItem
{
Value = "FR",
Text = "France",
Group = EuropeGroup
},
new SelectListItem
{
Value = "ES",
Text = "Spain",
Group = EuropeGroup
},
new SelectListItem
{
Value = "DE",
Text = "Germany",
Group = EuropeGroup
}
};
}

public string Country { get; set; }

public List<SelectListItem> Countries { get; }

Aquí mostramos los dos grupos:


El código HTML generado:

<form method="post" action="/Home/IndexGroup">


<select id="Country" name="Country">
<optgroup label="North America">
<option value="MEX">Mexico</option>
<option value="CAN">Canada</option>
<option value="US">USA</option>
</optgroup>
<optgroup label="Europe">
<option value="FR">France</option>
<option value="ES">Spain</option>
<option value="DE">Germany</option>
</optgroup>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

Selección múltiple
La aplicación auxiliar de etiquetas Select generará automáticamente el atributo multiple = "multiple" si la propiedad
especificada en el atributo asp-for es IEnumerable . Por ejemplo, si tenemos el siguiente modelo:

using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace FormsTagHelper.ViewModels
{
public class CountryViewModelIEnumerable
{
public IEnumerable<string> CountryCodes { get; set; }

public List<SelectListItem> Countries { get; } = new List<SelectListItem>


{
new SelectListItem { Value = "MX", Text = "Mexico" },
new SelectListItem { Value = "CA", Text = "Canada" },
new SelectListItem { Value = "US", Text = "USA" },
new SelectListItem { Value = "FR", Text = "France" },
new SelectListItem { Value = "ES", Text = "Spain" },
new SelectListItem { Value = "DE", Text = "Germany"}
};
}
}

Con la siguiente vista:


@model CountryViewModelIEnumerable

<form asp-controller="Home" asp-action="IndexMultiSelect" method="post">


<select asp-for="CountryCodes" asp-items="Model.Countries"></select>
<br /><button type="submit">Register</button>
</form>

Se genera el siguiente código HTML:

<form method="post" action="/Home/IndexMultiSelect">


<select id="CountryCodes"
multiple="multiple"
name="CountryCodes"><option value="MX">Mexico</option>
<option value="CA">Canada</option>
<option value="US">USA</option>
<option value="FR">France</option>
<option value="ES">Spain</option>
<option value="DE">Germany</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

Sin selección
Si ve que usa la opción "sin especificar" en varias páginas, puede crear una plantilla para no tener que repetir el
código HTML:

@model CountryViewModel

<form asp-controller="Home" asp-action="IndexEmpty" method="post">


@Html.EditorForModel()
<br /><button type="submit">Register</button>
</form>

La plantilla Views/Shared/EditorTemplates/CountryViewModel.cshtml:

@model CountryViewModel

<select asp-for="Country" asp-items="Model.Countries">


<option value="">--none--</option>
</select>

La posibilidad de agregar elementos HTML <option> no se limita exclusivamente a los casos en los que no se
seleccionada nada. Por ejemplo, el método de acción y vista siguientes generarán un código HTML similar al
código anterior:

public IActionResult IndexOption(int id)


{
var model = new CountryViewModel();
model.Country = "CA";
return View(model);
}
@model CountryViewModel

<form asp-controller="Home" asp-action="IndexEmpty" method="post">


<select asp-for="Country">
<option value="">&lt;none&gt;</option>
<option value="MX">Mexico</option>
<option value="CA">Canada</option>
<option value="US">USA</option>
</select>
<br /><button type="submit">Register</button>
</form>

Se seleccionará el elemento <option> correcto (que contenga el atributo selected="selected" ) en función del valor
real de Country .

<form method="post" action="/Home/IndexEmpty">


<select id="Country" name="Country">
<option value="">&lt;none&gt;</option>
<option value="MX">Mexico</option>
<option value="CA" selected="selected">Canada</option>
<option value="US">USA</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

Recursos adicionales
Aplicaciones auxiliares de etiquetas
HTML Form element (Elemento HTML Form)
Request Verification Token (Token de comprobación de solicitudes)
Enlace de modelos
Introduction to model validation in ASP.NET Core MVC (Introducción a la validación de modelos en ASP.NET
Core MVC )
IAttributeAdapter Interface (Interfaz IAttributeAdapter)
Fragmentos de código de este documento
Aplicaciones auxiliares de etiquetas en formularios de
ASP.NET Core
19/06/2018 • 29 minutes to read • Edit Online

Por Rick Anderson, Dave Paquette y Jerrie Pelser


En este documento se explica cómo trabajar con formularios y se detallan los elementos HTML que se usan
habitualmente en un formulario. El elemento HTML Form proporciona el mecanismo principal que las aplicaciones
web usan a la hora de devolver datos al servidor. La mayor parte de este documento se centra en describir las
aplicaciones auxiliares de etiquetas y cómo pueden servir para crear formularios HTML eficaces de manera
productiva. Se recomienda leer Introduction to Tag Helpers (Introducción a las aplicaciones auxiliares de etiquetas)
antes de este documento.
En muchos casos, las aplicaciones auxiliares HTML proporcionan un método alternativo a una aplicación auxiliar de
etiquetas específica, pero es importante tener en cuenta que las aplicaciones auxiliares de etiquetas no reemplazan
a las aplicaciones auxiliares HTML y que, de igual modo, no hay una aplicación auxiliar de etiqueta para cada
aplicación auxiliar HTML. Si existe una alternativa de aplicación auxiliar HTML, se mencionará aquí.

Aplicación auxiliar de etiquetas de formulario (Form)


La aplicación auxiliar de etiquetas Form hace lo siguiente:
Genera el valor de atributo action del elemento HTML <FORM> de una acción de controlador MVC o ruta
con nombre.
Genera un token comprobación de solicitudes oculto que impide que se falsifiquen solicitudes entre sitios
(cuando se usa con el atributo [ValidateAntiForgeryToken] en el método de acción HTTP Post).
Proporciona el atributo asp-route-<Parameter Name> , donde <Parameter Name> se agrega a los valores de
ruta. Los parámetros routeValues de Html.BeginForm y Html.BeginRouteForm proporcionan una
funcionalidad similar.
Tiene Html.BeginForm y Html.BeginRouteForm como alternativa de aplicación auxiliar HTML.
Ejemplo:

<form asp-controller="Demo" asp-action="Register" method="post">


<!-- Input and Submit elements -->
</form>

La aplicación auxiliar de etiquetas Form anterior genera el siguiente HTML:

<form method="post" action="/Demo/Register">


<!-- Input and Submit elements -->
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

El tiempo de ejecución MVC genera el valor de atributo action de los atributos de aplicación auxiliar de etiquetas
Form asp-controller y asp-action . La aplicación auxiliar de etiquetas Form genera también un token de
comprobación de solicitudes oculto que impide que se falsifiquen solicitudes entre sitios (cuando se usa con el
atributo [ValidateAntiForgeryToken] en el método de acción HTTP Post). Proteger un elemento HTML Form puro
de la falsificación de solicitudes entre sitios no es tarea fácil, y la aplicación auxiliar de etiquetas Form presta este
servicio.
Uso de una ruta con nombre
El atributo de aplicación auxiliar de etiquetas asp-route puede generar también el marcado del atributo HTML
action . Una aplicación con una ruta denominada register podría usar el siguiente marcado para la página de
registro:

<form asp-route="register" method="post">


<!-- Input and Submit elements -->
</form>

Muchas de las vistas de la carpeta Views/Account (que se genera cuando se crea una aplicación web con Cuentas
de usuario individuales) contienen el atributo asp-route-returnurl:

<form asp-controller="Account" asp-action="Login"


asp-route-returnurl="@ViewData["ReturnUrl"]"
method="post" class="form-horizontal" role="form">

NOTE
Con las plantillas integradas, returnUrl se rellena automáticamente solo cuando alguien intenta obtener acceso a un
recurso autorizado, pero no se ha autenticado o no tiene autorización. Si se intenta realizar un acceso no autorizado, el
middleware de seguridad redirige a la página de inicio de sesión con returnUrl configurado.

Aplicación auxiliar de etiquetas de entrada (Input)


La aplicación auxiliar de etiquetas Input enlaza un elemento HTML <input> a una expresión de modelo en la vista
de Razor.
Sintaxis:

<input asp-for="<Expression Name>" />

La aplicación auxiliar de etiquetas Input hace lo siguiente:


Genera los atributos HTML y name del nombre de expresión especificado en el atributo asp-for .
id
asp-for="Property1.Property2" es equivalente a m => m.Property1.Property2 . El nombre de una expresión es
lo que se usa para el valor de atributo asp-for . Vea la sección Nombres de expresión para obtener más
información.
Establece el valor de atributo HTML type según los atributos de tipo de modelo y de anotación de datos
aplicados a la propiedad de modelo.
No sobrescribirá el valor de atributo HTML type cuando se especifique uno.
Genera atributos de validación HTML5 a partir de los atributos de anotación de datos aplicados a las
propiedades de modelo.
Tiene características de aplicación auxiliar HTML que se superponen a Html.TextBoxFor y Html.EditorFor .
Vea la sección Alternativas de aplicación auxiliar HTML a la aplicación auxiliar de etiquetas Input
para obtener más información.
Permite establecer tipado fuerte. Si el nombre de la propiedad cambia y no se actualiza la aplicación auxiliar
de etiquetas, aparecerá un error similar al siguiente:

An error occurred during the compilation of a resource required to process


this request. Please review the following specific error details and modify
your source code appropriately.

Type expected
'RegisterViewModel' does not contain a definition for 'Email' and no
extension method 'Email' accepting a first argument of type 'RegisterViewModel'
could be found (are you missing a using directive or an assembly reference?)

La aplicación auxiliar de etiquetas Input establece el atributo HTML type en función del tipo .NET. En la siguiente
tabla se enumeran algunos tipos .NET habituales y el tipo HTML generado correspondiente (no incluimos aquí
todos los tipos .NET).

TIPO DE .NET TIPO DE ENTRADA

Bool type=”checkbox”

String type=”text”

DateTime type=”datetime”

Byte type=”number”

Valor int. type=”number”

Single, Double type=”number”

En la siguiente tabla se muestran algunos atributos de anotación de datos comunes que la aplicación auxiliar de
etiquetas Input asignará a tipos de entrada concretos (no incluimos aquí todos los atributo de validación):

ATRIBUTO TIPO DE ENTRADA

[EmailAddress] type=”email”

[Url] type=”url”

[HiddenInput] type=”hidden”

[Phone] type=”tel”

[DataType(DataType.Password)] type=”password”

[DataType(DataType.Date)] type=”date”

[DataType(DataType.Time)] type=”time”

Ejemplo:
using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }

[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
}

@model RegisterViewModel

<form asp-controller="Demo" asp-action="RegisterInput" method="post">


Email: <input asp-for="Email" /> <br />
Password: <input asp-for="Password" /><br />
<button type="submit">Register</button>
</form>

El código anterior genera el siguiente HTML:

<form method="post" action="/Demo/RegisterInput">


Email:
<input type="email" data-val="true"
data-val-email="The Email Address field is not a valid email address."
data-val-required="The Email Address field is required."
id="Email" name="Email" value="" /> <br>
Password:
<input type="password" data-val="true"
data-val-required="The Password field is required."
id="Password" name="Password" /><br>
<button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

Las anotaciones de datos que se aplican a las propiedades Email y Password generan metadatos en el modelo. La
aplicación auxiliar de etiquetas Input usa esos metadatos del modelo y genera atributos HTML5 data-val-* . Vea
Model Validation (Validación del modelo). Estos atributos describen los validadores que se van a adjuntar a los
campos de entrada, lo que proporciona HTML5 discreto y validación de jQuery. Los atributos discretos tienen el
formato data-val-rule="Error Message" , donde "rule" es el nombre de la regla de validación (como
data-val-required , data-val-email , data-val-maxlength , etc.). Si aparece un mensaje de error en el atributo, se
mostrará como el valor del atributo data-val-rule . También hay atributos con el formato
data-val-ruleName-argumentName="argumentValue" que aportan más información sobre la regla, por ejemplo,
data-val-maxlength-max="1024" .

Alternativas de aplicación auxiliar HTML a la aplicación auxiliar de etiquetas Input


Html.TextBox , Html.TextBoxFor , Html.Editor y Html.EditorFor tienen características que se superponen a la
aplicación auxiliar de etiquetas Input. La aplicación auxiliar de etiquetas Input establecerá automáticamente el
atributo type , cosa que no ocurrirá con Html.TextBox ni Html.TextBoxFor . Html.Editor y Html.EditorFor
controlan colecciones, objetos complejos y plantillas, pero la aplicación auxiliar de etiquetas Input no. La aplicación
auxiliar de etiquetas Input, Html.EditorFor y Html.TextBoxFor están fuertemente tipados (usan expresiones
lambda), pero Html.TextBox y Html.Editor no (usan nombres de expresión).
HtmlAttributes
@Html.Editor() y @Html.EditorFor() usan una entrada ViewDataDictionary especial denominada htmlAttributes
al ejecutar sus plantillas predeterminadas. Si lo desea, este comportamiento se puede enriquecer con parámetros
additionalViewData . En la clave "htmlAttributes" se distingue entre mayúsculas y minúsculas. La clave
"htmlAttributes" se controla de forma similar al objeto htmlAttributes pasado a aplicaciones auxiliares de etiqueta
Input como @Html.TextBox() .

@Html.EditorFor(model => model.YourProperty,


new { htmlAttributes = new { @class="myCssClass", style="Width:100px" } })

Nombres de expresión
El valor del atributo asp-for es una ModelExpression y la parte de la derecha de una expresión lambda. Por tanto,
asp-for="Property1" se convierte en m => m.Property1 en el código generado, motivo por el que no es necesario
incluir el prefijo Model . Puede usar el carácter "@" para iniciar una expresión insertada y moverla antes de `m.`:
</span><span class="sxs-lookup">You can use the "@" character to start an inline expression and move before
the m. :

@{
var joe = "Joe";
}
<input asp-for="@joe" />

Se genera el siguiente HTML:

<input type="text" id="joe" name="joe" value="Joe" />

Con las propiedades de colección, asp-for="CollectionProperty[23].Member" genera el mismo nombre que


asp-for="CollectionProperty[i].Member" si i tiene el valor 23 .
Cuando ASP.NET Core MVC calcula el valor de ModelExpression , inspecciona varios orígenes, ModelState incluido.
Fíjese en <input type="text" asp-for="@Name" /> . El atributo value calculado es el primer valor distinto de null de:
La entrada ModelState con la clave "Name".
El resultado de la expresión Model.Name .
Navegar a las propiedades secundarias
También se puede navegar a las propiedades secundarias a través de la ruta de acceso de propiedades del modelo
de vista. Pensemos en una clase de modelo más compleja que contiene una propiedad secundaria Address .

public class AddressViewModel


{
public string AddressLine1 { get; set; }
}
public class RegisterAddressViewModel
{
public string Email { get; set; }

[DataType(DataType.Password)]
public string Password { get; set; }

public AddressViewModel Address { get; set; }


}

En la vista, enlazamos a Address.AddressLine1 :

@model RegisterAddressViewModel

<form asp-controller="Demo" asp-action="RegisterAddress" method="post">


Email: <input asp-for="Email" /> <br />
Password: <input asp-for="Password" /><br />
Address: <input asp-for="Address.AddressLine1" /><br />
<button type="submit">Register</button>
</form>

Se genera el siguiente código HTML para Address.AddressLine1 :

<input type="text" id="Address_AddressLine1" name="Address.AddressLine1" value="" />

Colecciones y nombres de expresión


Como ejemplo, un modelo que contiene una matriz de Colors :

public class Person


{
public List<string> Colors { get; set; }

public int Age { get; set; }


}

El método de acción:

public IActionResult Edit(int id, int colorIndex)


{
ViewData["Index"] = colorIndex;
return View(GetPerson(id));
}

El siguiente código de Razor muestra cómo se tiene acceso a un elemento Color concreto:

@model Person
@{
var index = (int)ViewData["index"];
}

<form asp-controller="ToDo" asp-action="Edit" method="post">


@Html.EditorFor(m => m.Colors[index])
<label asp-for="Age"></label>
<input asp-for="Age" /><br />
<button type="submit">Post</button>
</form>
La plantilla Views/Shared/EditorTemplates/String.cshtml:

@model string

<label asp-for="@Model"></label>
<input asp-for="@Model" /> <br />

Ejemplo en el que se usa List<T> :

public class ToDoItem


{
public string Name { get; set; }

public bool IsDone { get; set; }


}

El siguiente código de Razor muestra cómo iterar por una colección:

@model List<ToDoItem>

<form asp-controller="ToDo" asp-action="Edit" method="post">


<table>
<tr> <th>Name</th> <th>Is Done</th> </tr>

@for (int i = 0; i < Model.Count; i++)


{
<tr>
@Html.EditorFor(model => model[i])
</tr>
}

</table>
<button type="submit">Save</button>
</form>

La plantilla Views/Shared/EditorTemplates/ToDoItem.cshtml:

@model ToDoItem

<td>
<label asp-for="@Model.Name"></label>
@Html.DisplayFor(model => model.Name)
</td>
<td>
<input asp-for="@Model.IsDone" />
</td>

@*
This template replaces the following Razor which evaluates the indexer three times.
<td>
<label asp-for="@Model[i].Name"></label>
@Html.DisplayFor(model => model[i].Name)
</td>
<td>
<input asp-for="@Model[i].IsDone" />
</td>
*@
NOTE
Use siempre for (y no foreach ) para iterar por una lista. Evaluar un indizador en una expresión de LINQ puede ser
costoso, con lo cual esa posibilidad hay que reducirla al mínimo.

NOTE
El código de ejemplo comentado anterior muestra cómo reemplazaríamos la expresión lambda por el operador @ para tener
acceso a cada elemento ToDoItem de la lista.

Aplicación auxiliar de etiquetas de área de texto (Textarea)


La aplicación auxiliar de etiquetas Textarea Tag Helper es similar a la aplicación auxiliar de etiquetas Input.
Genera los atributos id y name , y los atributos de validación de datos del modelo de un elemento
<textarea>.
Permite establecer tipado fuerte.
Alternativa de aplicación auxiliar HTML: Html.TextAreaFor .

Ejemplo:

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class DescriptionViewModel
{
[MinLength(5)]
[MaxLength(1024)]
public string Description { get; set; }
}
}

@model DescriptionViewModel

<form asp-controller="Demo" asp-action="RegisterTextArea" method="post">


<textarea asp-for="Description"></textarea>
<button type="submit">Test</button>
</form>

Se genera el siguiente código HTML:


<form method="post" action="/Demo/RegisterTextArea">
<textarea data-val="true"
data-val-maxlength="The field Description must be a string or array type with a maximum length of
&#x27;1024&#x27;."
data-val-maxlength-max="1024"
data-val-minlength="The field Description must be a string or array type with a minimum length of
&#x27;5&#x27;."
data-val-minlength-min="5"
id="Description" name="Description">
</textarea>
<button type="submit">Test</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

Aplicación auxiliar de etiquetas Label


Genera el título de la etiqueta y el atributo for en un elemento de un nombre de expresión.
Alternativa de aplicación auxiliar HTML: Html.LabelFor .

Label Tag Helper proporciona las siguientes ventajas con respecto a un elemento HTML Label puro:
Obtendrá automáticamente el valor de la etiqueta descriptiva del atributo Display . El nombre para mostrar
que se busca puede cambiar con el tiempo y la combinación del atributo Display , y la aplicación auxiliar de
etiquetas Label aplicará el elemento Display en cualquier lugar donde se use.
El código fuente contiene menos marcado.
Permite establecer tipado fuerte con la propiedad de modelo.
Ejemplo:

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class SimpleViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }
}
}

@model SimpleViewModel

<form asp-controller="Demo" asp-action="RegisterLabel" method="post">


<label asp-for="Email"></label>
<input asp-for="Email" /> <br />
</form>

Se genera el siguiente código HTML para el elemento <label> :

<label for="Email">Email Address</label>

La aplicación auxiliar de etiquetas Label genera el valor de atributo for de "Email", que es el identificador asociado
al elemento <input> . Las aplicaciones auxiliares de etiqueta generan elementos id y for coherentes para que se
puedan asociar correctamente. El título de este ejemplo proviene del atributo Display . Si el modelo no contuviera
un atributo Display , el título sería el nombre de propiedad de la expresión.

Aplicaciones auxiliares de etiquetas de validación


Hay dos aplicaciones auxiliares de etiquetas de validación: Validation Message Tag Helper (que muestra un mensaje
de validación relativo a una única propiedad del modelo) y Validation Summary Tag Helper (que muestra un
resumen de los errores de validación). Input Tag Helper agrega atributos de validación del lado cliente HTML5 a
los elementos de entrada en función de los atributos de anotación de datos de las clases del modelo. La validación
también se realiza en el lado servidor. La aplicación auxiliar de etiquetas de validación muestra estos mensajes de
error cuando se produce un error de validación.
Aplicación auxiliar de etiquetas de mensaje de validación
Agrega el atributo HTML5 data-valmsg-for="property" al elemento span, que adjunta los mensajes de error
de validación en el campo de entrada de la propiedad de modelo especificada. Cuando se produce un error
de validación en el lado cliente, jQuery muestra el mensaje de error en el elemento <span> .
La validación también tiene lugar en el lado servidor. Puede que JavaScript esté deshabilitado en los clientes,
mientras que hay algunas validaciones que solo se pueden realizar en el lado servidor.
Alternativa de aplicación auxiliar HTML: Html.ValidationMessageFor .

Validation Message Tag Helper se usa con el atributo asp-validation-for en un elemento HTML span.

<span asp-validation-for="Email"></span>

La aplicación auxiliar de etiquetas de mensaje de validación generará el siguiente código HTML:

<span class="field-validation-valid"
data-valmsg-for="Email"
data-valmsg-replace="true"></span>

Validation Message Tag Helper suele usarse por lo general después de una aplicación auxiliar de etiquetas Input
en la misma propiedad. Gracias a esto, se mostrarán todos los mensajes de error de validación cerca de la entrada
que produjo el error.

NOTE
Debe tener una vista con las referencias de script de JavaScript y de jQuery adecuadas para la validación del lado cliente. Para
más información, vea Introduction to model validation in ASP.NET Core MVC (Introducción a la validación de modelos en
ASP.NET Core MVC).

Cuando se produce un error de validación del lado servidor (por ejemplo, porque haya una validación del lado
servidor personalizada o porque la validación del lado cliente esté deshabilitada), MVC pone ese mensaje de error
como cuerpo del elemento <span> .

<span class="field-validation-error" data-valmsg-for="Email"


data-valmsg-replace="true">
The Email Address field is required.
</span>

Aplicación auxiliar de etiquetas de resumen de validación


Tiene como destino todos los elementos <div> con el atributo asp-validation-summary .
Alternativa de aplicación auxiliar HTML: @Html.ValidationSummary .

Validation Summary Tag Helper se usa para mostrar un resumen de los mensajes de validación. El valor de atributo
asp-validation-summary puede ser cualquiera de los siguientes:

ASP-VALIDATION-SUMMARY MENSAJES DE VALIDACIÓN QUE SE MUESTRAN

ValidationSummary.All Nivel de modelo y de propiedad

ValidationSummary.ModelOnly Modelo

ValidationSummary.None Ninguna

Ejemplo
En el siguiente ejemplo, el modelo de datos se complementa con atributos DataAnnotation , lo que genera mensajes
de error de validación sobre el elemento <input> . Cuando se produce un error de validación, la aplicación auxiliar
de etiquetas de validación muestra el mensaje de error:

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }

[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
}

@model RegisterViewModel

<form asp-controller="Demo" asp-action="RegisterValidation" method="post">


<div asp-validation-summary="ModelOnly"></div>
Email: <input asp-for="Email" /> <br />
<span asp-validation-for="Email"></span><br />
Password: <input asp-for="Password" /><br />
<span asp-validation-for="Password"></span><br />
<button type="submit">Register</button>
</form>

El código HTML generado (cuando el modelo es válido) es este:


<form action="/DemoReg/Register" method="post">
<div class="validation-summary-valid" data-valmsg-summary="true">
<ul><li style="display:none"></li></ul></div>
Email: <input name="Email" id="Email" type="email" value=""
data-val-required="The Email field is required."
data-val-email="The Email field is not a valid email address."
data-val="true"> <br>
<span class="field-validation-valid" data-valmsg-replace="true"
data-valmsg-for="Email"></span><br>
Password: <input name="Password" id="Password" type="password"
data-val-required="The Password field is required." data-val="true"><br>
<span class="field-validation-valid" data-valmsg-replace="true"
data-valmsg-for="Password"></span><br>
<button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

Aplicación auxiliar de etiquetas de selección (Select)


Genera el elemento select y el elemento asociado option de las propiedades del modelo.
Tiene Html.DropDownListFor y Html.ListBoxFor como alternativa de aplicación auxiliar HTML.
El elemento asp-for de Select Tag Helper especifica el nombre de la propiedad de modelo del elemento select,
mientras que asp-items especifica los elementos option. Por ejemplo:

<select asp-for="Country" asp-items="Model.Countries"></select>

Ejemplo:

using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace FormsTagHelper.ViewModels
{
public class CountryViewModel
{
public string Country { get; set; }

public List<SelectListItem> Countries { get; } = new List<SelectListItem>


{
new SelectListItem { Value = "MX", Text = "Mexico" },
new SelectListItem { Value = "CA", Text = "Canada" },
new SelectListItem { Value = "US", Text = "USA" },
};
}
}

El método Index inicializa CountryViewModel , establece el país seleccionado y lo pasa a la vista Index .

public IActionResult IndexOption(int id)


{
var model = new CountryViewModel();
model.Country = "CA";
return View(model);
}

El método HTTP POST Index muestra la selección:


[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Index(CountryViewModel model)
{
if (ModelState.IsValid)
{
var msg = model.Country + " selected";
return RedirectToAction("IndexSuccess", new { message = msg});
}

// If we got this far, something failed; redisplay form.


return View(model);
}

La vista Index :

@model CountryViewModel

<form asp-controller="Home" asp-action="Index" method="post">


<select asp-for="Country" asp-items="Model.Countries"></select>
<br /><button type="submit">Register</button>
</form>

Genera el siguiente código HTML (con la selección "CA"):

<form method="post" action="/">


<select id="Country" name="Country">
<option value="MX">Mexico</option>
<option selected="selected" value="CA">Canada</option>
<option value="US">USA</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

NOTE
No se recomienda usar ViewBag o ViewData con la aplicación auxiliar de etiquetas Select. Un modelo de vista es más eficaz
a la hora de proporcionar metadatos MVC y suele ser menos problemático.

El valor de atributo asp-for es un caso especial y no necesita un prefijo Model , mientras que los otros atributos de
aplicación auxiliar de etiquetas sí (como asp-items ).

<select asp-for="Country" asp-items="Model.Countries"></select>

Enlace con enum


A menudo, conviene usar <select> con una propiedad enum y generar los elementos SelectListItem a partir de
valores enum .
Ejemplo:
public class CountryEnumViewModel
{
public CountryEnum EnumCountry { get; set; }
}
}

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public enum CountryEnum
{
[Display(Name = "United Mexican States")]
Mexico,
[Display(Name = "United States of America")]
USA,
Canada,
France,
Germany,
Spain
}
}

El método GetEnumSelectList genera un objeto SelectList para una enumeración.

@model CountryEnumViewModel

<form asp-controller="Home" asp-action="IndexEnum" method="post">


<select asp-for="EnumCountry"
asp-items="Html.GetEnumSelectList<CountryEnum>()"> >
</select>
<br /><button type="submit">Register</button>
</form>

Puede complementar la lista de enumeradores con el atributo Display para obtener una interfaz de usuario más
completa:

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public enum CountryEnum
{
[Display(Name = "United Mexican States")]
Mexico,
[Display(Name = "United States of America")]
USA,
Canada,
France,
Germany,
Spain
}
}

Se genera el siguiente código HTML:


<form method="post" action="/Home/IndexEnum">
<select data-val="true" data-val-required="The EnumCountry field is required."
id="EnumCountry" name="EnumCountry">
<option value="0">United Mexican States</option>
<option value="1">United States of America</option>
<option value="2">Canada</option>
<option value="3">France</option>
<option value="4">Germany</option>
<option selected="selected" value="5">Spain</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

Agrupamiento de opciones
El elemento HTML <optgroup> se genera cuando el modelo de vista contiene uno o varios objetos
SelectListGroup .

CountryViewModelGroup agrupa los elementos SelectListItem en los grupos "North America" y "Europe":
public class CountryViewModelGroup
{
public CountryViewModelGroup()
{
var NorthAmericaGroup = new SelectListGroup { Name = "North America" };
var EuropeGroup = new SelectListGroup { Name = "Europe" };

Countries = new List<SelectListItem>


{
new SelectListItem
{
Value = "MEX",
Text = "Mexico",
Group = NorthAmericaGroup
},
new SelectListItem
{
Value = "CAN",
Text = "Canada",
Group = NorthAmericaGroup
},
new SelectListItem
{
Value = "US",
Text = "USA",
Group = NorthAmericaGroup
},
new SelectListItem
{
Value = "FR",
Text = "France",
Group = EuropeGroup
},
new SelectListItem
{
Value = "ES",
Text = "Spain",
Group = EuropeGroup
},
new SelectListItem
{
Value = "DE",
Text = "Germany",
Group = EuropeGroup
}
};
}

public string Country { get; set; }

public List<SelectListItem> Countries { get; }

Aquí mostramos los dos grupos:


El código HTML generado:

<form method="post" action="/Home/IndexGroup">


<select id="Country" name="Country">
<optgroup label="North America">
<option value="MEX">Mexico</option>
<option value="CAN">Canada</option>
<option value="US">USA</option>
</optgroup>
<optgroup label="Europe">
<option value="FR">France</option>
<option value="ES">Spain</option>
<option value="DE">Germany</option>
</optgroup>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

Selección múltiple
La aplicación auxiliar de etiquetas Select generará automáticamente el atributo multiple = "multiple" si la propiedad
especificada en el atributo asp-for es IEnumerable . Por ejemplo, si tenemos el siguiente modelo:

using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace FormsTagHelper.ViewModels
{
public class CountryViewModelIEnumerable
{
public IEnumerable<string> CountryCodes { get; set; }

public List<SelectListItem> Countries { get; } = new List<SelectListItem>


{
new SelectListItem { Value = "MX", Text = "Mexico" },
new SelectListItem { Value = "CA", Text = "Canada" },
new SelectListItem { Value = "US", Text = "USA" },
new SelectListItem { Value = "FR", Text = "France" },
new SelectListItem { Value = "ES", Text = "Spain" },
new SelectListItem { Value = "DE", Text = "Germany"}
};
}
}

Con la siguiente vista:


@model CountryViewModelIEnumerable

<form asp-controller="Home" asp-action="IndexMultiSelect" method="post">


<select asp-for="CountryCodes" asp-items="Model.Countries"></select>
<br /><button type="submit">Register</button>
</form>

Se genera el siguiente código HTML:

<form method="post" action="/Home/IndexMultiSelect">


<select id="CountryCodes"
multiple="multiple"
name="CountryCodes"><option value="MX">Mexico</option>
<option value="CA">Canada</option>
<option value="US">USA</option>
<option value="FR">France</option>
<option value="ES">Spain</option>
<option value="DE">Germany</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

Sin selección
Si ve que usa la opción "sin especificar" en varias páginas, puede crear una plantilla para no tener que repetir el
código HTML:

@model CountryViewModel

<form asp-controller="Home" asp-action="IndexEmpty" method="post">


@Html.EditorForModel()
<br /><button type="submit">Register</button>
</form>

La plantilla Views/Shared/EditorTemplates/CountryViewModel.cshtml:

@model CountryViewModel

<select asp-for="Country" asp-items="Model.Countries">


<option value="">--none--</option>
</select>

La posibilidad de agregar elementos HTML <option> no se limita exclusivamente a los casos en los que no se
seleccionada nada. Por ejemplo, el método de acción y vista siguientes generarán un código HTML similar al
código anterior:

public IActionResult IndexOption(int id)


{
var model = new CountryViewModel();
model.Country = "CA";
return View(model);
}
@model CountryViewModel

<form asp-controller="Home" asp-action="IndexEmpty" method="post">


<select asp-for="Country">
<option value="">&lt;none&gt;</option>
<option value="MX">Mexico</option>
<option value="CA">Canada</option>
<option value="US">USA</option>
</select>
<br /><button type="submit">Register</button>
</form>

Se seleccionará el elemento <option> correcto (que contenga el atributo selected="selected" ) en función del valor
real de Country .

<form method="post" action="/Home/IndexEmpty">


<select id="Country" name="Country">
<option value="">&lt;none&gt;</option>
<option value="MX">Mexico</option>
<option value="CA" selected="selected">Canada</option>
<option value="US">USA</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

Recursos adicionales
Aplicaciones auxiliares de etiquetas
HTML Form element (Elemento HTML Form)
Request Verification Token (Token de comprobación de solicitudes)
Enlace de modelos
Introduction to model validation in ASP.NET Core MVC (Introducción a la validación de modelos en ASP.NET
Core MVC )
IAttributeAdapter Interface (Interfaz IAttributeAdapter)
Fragmentos de código de este documento
Aplicaciones auxiliares de etiquetas en formularios de
ASP.NET Core
19/06/2018 • 29 minutes to read • Edit Online

Por Rick Anderson, Dave Paquette y Jerrie Pelser


En este documento se explica cómo trabajar con formularios y se detallan los elementos HTML que se usan
habitualmente en un formulario. El elemento HTML Form proporciona el mecanismo principal que las aplicaciones
web usan a la hora de devolver datos al servidor. La mayor parte de este documento se centra en describir las
aplicaciones auxiliares de etiquetas y cómo pueden servir para crear formularios HTML eficaces de manera
productiva. Se recomienda leer Introduction to Tag Helpers (Introducción a las aplicaciones auxiliares de etiquetas)
antes de este documento.
En muchos casos, las aplicaciones auxiliares HTML proporcionan un método alternativo a una aplicación auxiliar de
etiquetas específica, pero es importante tener en cuenta que las aplicaciones auxiliares de etiquetas no reemplazan
a las aplicaciones auxiliares HTML y que, de igual modo, no hay una aplicación auxiliar de etiqueta para cada
aplicación auxiliar HTML. Si existe una alternativa de aplicación auxiliar HTML, se mencionará aquí.

Aplicación auxiliar de etiquetas de formulario (Form)


La aplicación auxiliar de etiquetas Form hace lo siguiente:
Genera el valor de atributo action del elemento HTML <FORM> de una acción de controlador MVC o ruta
con nombre.
Genera un token comprobación de solicitudes oculto que impide que se falsifiquen solicitudes entre sitios
(cuando se usa con el atributo [ValidateAntiForgeryToken] en el método de acción HTTP Post).
Proporciona el atributo asp-route-<Parameter Name> , donde <Parameter Name> se agrega a los valores de
ruta. Los parámetros routeValues de Html.BeginForm y Html.BeginRouteForm proporcionan una
funcionalidad similar.
Tiene Html.BeginForm y Html.BeginRouteForm como alternativa de aplicación auxiliar HTML.
Ejemplo:

<form asp-controller="Demo" asp-action="Register" method="post">


<!-- Input and Submit elements -->
</form>

La aplicación auxiliar de etiquetas Form anterior genera el siguiente HTML:

<form method="post" action="/Demo/Register">


<!-- Input and Submit elements -->
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

El tiempo de ejecución MVC genera el valor de atributo action de los atributos de aplicación auxiliar de etiquetas
Form asp-controller y asp-action . La aplicación auxiliar de etiquetas Form genera también un token de
comprobación de solicitudes oculto que impide que se falsifiquen solicitudes entre sitios (cuando se usa con el
atributo [ValidateAntiForgeryToken] en el método de acción HTTP Post). Proteger un elemento HTML Form puro
de la falsificación de solicitudes entre sitios no es tarea fácil, y la aplicación auxiliar de etiquetas Form presta este
servicio.
Uso de una ruta con nombre
El atributo de aplicación auxiliar de etiquetas asp-route puede generar también el marcado del atributo HTML
action . Una aplicación con una ruta denominada register podría usar el siguiente marcado para la página de
registro:

<form asp-route="register" method="post">


<!-- Input and Submit elements -->
</form>

Muchas de las vistas de la carpeta Views/Account (que se genera cuando se crea una aplicación web con Cuentas
de usuario individuales) contienen el atributo asp-route-returnurl:

<form asp-controller="Account" asp-action="Login"


asp-route-returnurl="@ViewData["ReturnUrl"]"
method="post" class="form-horizontal" role="form">

NOTE
Con las plantillas integradas, returnUrl se rellena automáticamente solo cuando alguien intenta obtener acceso a un
recurso autorizado, pero no se ha autenticado o no tiene autorización. Si se intenta realizar un acceso no autorizado, el
middleware de seguridad redirige a la página de inicio de sesión con returnUrl configurado.

Aplicación auxiliar de etiquetas de entrada (Input)


La aplicación auxiliar de etiquetas Input enlaza un elemento HTML <input> a una expresión de modelo en la vista
de Razor.
Sintaxis:

<input asp-for="<Expression Name>" />

La aplicación auxiliar de etiquetas Input hace lo siguiente:


Genera los atributos HTML y name del nombre de expresión especificado en el atributo asp-for .
id
asp-for="Property1.Property2" es equivalente a m => m.Property1.Property2 . El nombre de una expresión es
lo que se usa para el valor de atributo asp-for . Vea la sección Nombres de expresión para obtener más
información.
Establece el valor de atributo HTML type según los atributos de tipo de modelo y de anotación de datos
aplicados a la propiedad de modelo.
No sobrescribirá el valor de atributo HTML type cuando se especifique uno.
Genera atributos de validación HTML5 a partir de los atributos de anotación de datos aplicados a las
propiedades de modelo.
Tiene características de aplicación auxiliar HTML que se superponen a Html.TextBoxFor y Html.EditorFor .
Vea la sección Alternativas de aplicación auxiliar HTML a la aplicación auxiliar de etiquetas Input
para obtener más información.
Permite establecer tipado fuerte. Si el nombre de la propiedad cambia y no se actualiza la aplicación auxiliar
de etiquetas, aparecerá un error similar al siguiente:

An error occurred during the compilation of a resource required to process


this request. Please review the following specific error details and modify
your source code appropriately.

Type expected
'RegisterViewModel' does not contain a definition for 'Email' and no
extension method 'Email' accepting a first argument of type 'RegisterViewModel'
could be found (are you missing a using directive or an assembly reference?)

La aplicación auxiliar de etiquetas Input establece el atributo HTML type en función del tipo .NET. En la siguiente
tabla se enumeran algunos tipos .NET habituales y el tipo HTML generado correspondiente (no incluimos aquí
todos los tipos .NET).

TIPO DE .NET TIPO DE ENTRADA

Bool type=”checkbox”

String type=”text”

DateTime type=”datetime”

Byte type=”number”

Valor int. type=”number”

Single, Double type=”number”

En la siguiente tabla se muestran algunos atributos de anotación de datos comunes que la aplicación auxiliar de
etiquetas Input asignará a tipos de entrada concretos (no incluimos aquí todos los atributo de validación):

ATRIBUTO TIPO DE ENTRADA

[EmailAddress] type=”email”

[Url] type=”url”

[HiddenInput] type=”hidden”

[Phone] type=”tel”

[DataType(DataType.Password)] type=”password”

[DataType(DataType.Date)] type=”date”

[DataType(DataType.Time)] type=”time”

Ejemplo:
using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }

[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
}

@model RegisterViewModel

<form asp-controller="Demo" asp-action="RegisterInput" method="post">


Email: <input asp-for="Email" /> <br />
Password: <input asp-for="Password" /><br />
<button type="submit">Register</button>
</form>

El código anterior genera el siguiente HTML:

<form method="post" action="/Demo/RegisterInput">


Email:
<input type="email" data-val="true"
data-val-email="The Email Address field is not a valid email address."
data-val-required="The Email Address field is required."
id="Email" name="Email" value="" /> <br>
Password:
<input type="password" data-val="true"
data-val-required="The Password field is required."
id="Password" name="Password" /><br>
<button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

Las anotaciones de datos que se aplican a las propiedades Email y Password generan metadatos en el modelo. La
aplicación auxiliar de etiquetas Input usa esos metadatos del modelo y genera atributos HTML5 data-val-* . Vea
Model Validation (Validación del modelo). Estos atributos describen los validadores que se van a adjuntar a los
campos de entrada, lo que proporciona HTML5 discreto y validación de jQuery. Los atributos discretos tienen el
formato data-val-rule="Error Message" , donde "rule" es el nombre de la regla de validación (como
data-val-required , data-val-email , data-val-maxlength , etc.). Si aparece un mensaje de error en el atributo, se
mostrará como el valor del atributo data-val-rule . También hay atributos con el formato
data-val-ruleName-argumentName="argumentValue" que aportan más información sobre la regla, por ejemplo,
data-val-maxlength-max="1024" .

Alternativas de aplicación auxiliar HTML a la aplicación auxiliar de etiquetas Input


Html.TextBox , Html.TextBoxFor , Html.Editor y Html.EditorFor tienen características que se superponen a la
aplicación auxiliar de etiquetas Input. La aplicación auxiliar de etiquetas Input establecerá automáticamente el
atributo type , cosa que no ocurrirá con Html.TextBox ni Html.TextBoxFor . Html.Editor y Html.EditorFor
controlan colecciones, objetos complejos y plantillas, pero la aplicación auxiliar de etiquetas Input no. La aplicación
auxiliar de etiquetas Input, Html.EditorFor y Html.TextBoxFor están fuertemente tipados (usan expresiones
lambda), pero Html.TextBox y Html.Editor no (usan nombres de expresión).
HtmlAttributes
@Html.Editor() y @Html.EditorFor() usan una entrada ViewDataDictionary especial denominada htmlAttributes
al ejecutar sus plantillas predeterminadas. Si lo desea, este comportamiento se puede enriquecer con parámetros
additionalViewData . En la clave "htmlAttributes" se distingue entre mayúsculas y minúsculas. La clave
"htmlAttributes" se controla de forma similar al objeto htmlAttributes pasado a aplicaciones auxiliares de etiqueta
Input como @Html.TextBox() .

@Html.EditorFor(model => model.YourProperty,


new { htmlAttributes = new { @class="myCssClass", style="Width:100px" } })

Nombres de expresión
El valor del atributo asp-for es una ModelExpression y la parte de la derecha de una expresión lambda. Por tanto,
asp-for="Property1" se convierte en m => m.Property1 en el código generado, motivo por el que no es necesario
incluir el prefijo Model . Puede usar el carácter "@" para iniciar una expresión insertada y moverla antes de `m.`:
</span><span class="sxs-lookup">You can use the "@" character to start an inline expression and move before
the m. :

@{
var joe = "Joe";
}
<input asp-for="@joe" />

Se genera el siguiente HTML:

<input type="text" id="joe" name="joe" value="Joe" />

Con las propiedades de colección, asp-for="CollectionProperty[23].Member" genera el mismo nombre que


asp-for="CollectionProperty[i].Member" si i tiene el valor 23 .
Cuando ASP.NET Core MVC calcula el valor de ModelExpression , inspecciona varios orígenes, ModelState incluido.
Fíjese en <input type="text" asp-for="@Name" /> . El atributo value calculado es el primer valor distinto de null de:
La entrada ModelState con la clave "Name".
El resultado de la expresión Model.Name .
Navegar a las propiedades secundarias
También se puede navegar a las propiedades secundarias a través de la ruta de acceso de propiedades del modelo
de vista. Pensemos en una clase de modelo más compleja que contiene una propiedad secundaria Address .

public class AddressViewModel


{
public string AddressLine1 { get; set; }
}
public class RegisterAddressViewModel
{
public string Email { get; set; }

[DataType(DataType.Password)]
public string Password { get; set; }

public AddressViewModel Address { get; set; }


}

En la vista, enlazamos a Address.AddressLine1 :

@model RegisterAddressViewModel

<form asp-controller="Demo" asp-action="RegisterAddress" method="post">


Email: <input asp-for="Email" /> <br />
Password: <input asp-for="Password" /><br />
Address: <input asp-for="Address.AddressLine1" /><br />
<button type="submit">Register</button>
</form>

Se genera el siguiente código HTML para Address.AddressLine1 :

<input type="text" id="Address_AddressLine1" name="Address.AddressLine1" value="" />

Colecciones y nombres de expresión


Como ejemplo, un modelo que contiene una matriz de Colors :

public class Person


{
public List<string> Colors { get; set; }

public int Age { get; set; }


}

El método de acción:

public IActionResult Edit(int id, int colorIndex)


{
ViewData["Index"] = colorIndex;
return View(GetPerson(id));
}

El siguiente código de Razor muestra cómo se tiene acceso a un elemento Color concreto:

@model Person
@{
var index = (int)ViewData["index"];
}

<form asp-controller="ToDo" asp-action="Edit" method="post">


@Html.EditorFor(m => m.Colors[index])
<label asp-for="Age"></label>
<input asp-for="Age" /><br />
<button type="submit">Post</button>
</form>
La plantilla Views/Shared/EditorTemplates/String.cshtml:

@model string

<label asp-for="@Model"></label>
<input asp-for="@Model" /> <br />

Ejemplo en el que se usa List<T> :

public class ToDoItem


{
public string Name { get; set; }

public bool IsDone { get; set; }


}

El siguiente código de Razor muestra cómo iterar por una colección:

@model List<ToDoItem>

<form asp-controller="ToDo" asp-action="Edit" method="post">


<table>
<tr> <th>Name</th> <th>Is Done</th> </tr>

@for (int i = 0; i < Model.Count; i++)


{
<tr>
@Html.EditorFor(model => model[i])
</tr>
}

</table>
<button type="submit">Save</button>
</form>

La plantilla Views/Shared/EditorTemplates/ToDoItem.cshtml:

@model ToDoItem

<td>
<label asp-for="@Model.Name"></label>
@Html.DisplayFor(model => model.Name)
</td>
<td>
<input asp-for="@Model.IsDone" />
</td>

@*
This template replaces the following Razor which evaluates the indexer three times.
<td>
<label asp-for="@Model[i].Name"></label>
@Html.DisplayFor(model => model[i].Name)
</td>
<td>
<input asp-for="@Model[i].IsDone" />
</td>
*@
NOTE
Use siempre for (y no foreach ) para iterar por una lista. Evaluar un indizador en una expresión de LINQ puede ser
costoso, con lo cual esa posibilidad hay que reducirla al mínimo.

NOTE
El código de ejemplo comentado anterior muestra cómo reemplazaríamos la expresión lambda por el operador @ para tener
acceso a cada elemento ToDoItem de la lista.

Aplicación auxiliar de etiquetas de área de texto (Textarea)


La aplicación auxiliar de etiquetas Textarea Tag Helper es similar a la aplicación auxiliar de etiquetas Input.
Genera los atributos id y name , y los atributos de validación de datos del modelo de un elemento
<textarea>.
Permite establecer tipado fuerte.
Alternativa de aplicación auxiliar HTML: Html.TextAreaFor .

Ejemplo:

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class DescriptionViewModel
{
[MinLength(5)]
[MaxLength(1024)]
public string Description { get; set; }
}
}

@model DescriptionViewModel

<form asp-controller="Demo" asp-action="RegisterTextArea" method="post">


<textarea asp-for="Description"></textarea>
<button type="submit">Test</button>
</form>

Se genera el siguiente código HTML:


<form method="post" action="/Demo/RegisterTextArea">
<textarea data-val="true"
data-val-maxlength="The field Description must be a string or array type with a maximum length of
&#x27;1024&#x27;."
data-val-maxlength-max="1024"
data-val-minlength="The field Description must be a string or array type with a minimum length of
&#x27;5&#x27;."
data-val-minlength-min="5"
id="Description" name="Description">
</textarea>
<button type="submit">Test</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

Aplicación auxiliar de etiquetas Label


Genera el título de la etiqueta y el atributo for en un elemento de un nombre de expresión.
Alternativa de aplicación auxiliar HTML: Html.LabelFor .

Label Tag Helper proporciona las siguientes ventajas con respecto a un elemento HTML Label puro:
Obtendrá automáticamente el valor de la etiqueta descriptiva del atributo Display . El nombre para mostrar
que se busca puede cambiar con el tiempo y la combinación del atributo Display , y la aplicación auxiliar de
etiquetas Label aplicará el elemento Display en cualquier lugar donde se use.
El código fuente contiene menos marcado.
Permite establecer tipado fuerte con la propiedad de modelo.
Ejemplo:

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class SimpleViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }
}
}

@model SimpleViewModel

<form asp-controller="Demo" asp-action="RegisterLabel" method="post">


<label asp-for="Email"></label>
<input asp-for="Email" /> <br />
</form>

Se genera el siguiente código HTML para el elemento <label> :

<label for="Email">Email Address</label>

La aplicación auxiliar de etiquetas Label genera el valor de atributo for de "Email", que es el identificador asociado
al elemento <input> . Las aplicaciones auxiliares de etiqueta generan elementos id y for coherentes para que se
puedan asociar correctamente. El título de este ejemplo proviene del atributo Display . Si el modelo no contuviera
un atributo Display , el título sería el nombre de propiedad de la expresión.

Aplicaciones auxiliares de etiquetas de validación


Hay dos aplicaciones auxiliares de etiquetas de validación: Validation Message Tag Helper (que muestra un mensaje
de validación relativo a una única propiedad del modelo) y Validation Summary Tag Helper (que muestra un
resumen de los errores de validación). Input Tag Helper agrega atributos de validación del lado cliente HTML5 a
los elementos de entrada en función de los atributos de anotación de datos de las clases del modelo. La validación
también se realiza en el lado servidor. La aplicación auxiliar de etiquetas de validación muestra estos mensajes de
error cuando se produce un error de validación.
Aplicación auxiliar de etiquetas de mensaje de validación
Agrega el atributo HTML5 data-valmsg-for="property" al elemento span, que adjunta los mensajes de error
de validación en el campo de entrada de la propiedad de modelo especificada. Cuando se produce un error
de validación en el lado cliente, jQuery muestra el mensaje de error en el elemento <span> .
La validación también tiene lugar en el lado servidor. Puede que JavaScript esté deshabilitado en los clientes,
mientras que hay algunas validaciones que solo se pueden realizar en el lado servidor.
Alternativa de aplicación auxiliar HTML: Html.ValidationMessageFor .

Validation Message Tag Helper se usa con el atributo asp-validation-for en un elemento HTML span.

<span asp-validation-for="Email"></span>

La aplicación auxiliar de etiquetas de mensaje de validación generará el siguiente código HTML:

<span class="field-validation-valid"
data-valmsg-for="Email"
data-valmsg-replace="true"></span>

Validation Message Tag Helper suele usarse por lo general después de una aplicación auxiliar de etiquetas Input
en la misma propiedad. Gracias a esto, se mostrarán todos los mensajes de error de validación cerca de la entrada
que produjo el error.

NOTE
Debe tener una vista con las referencias de script de JavaScript y de jQuery adecuadas para la validación del lado cliente. Para
más información, vea Introduction to model validation in ASP.NET Core MVC (Introducción a la validación de modelos en
ASP.NET Core MVC).

Cuando se produce un error de validación del lado servidor (por ejemplo, porque haya una validación del lado
servidor personalizada o porque la validación del lado cliente esté deshabilitada), MVC pone ese mensaje de error
como cuerpo del elemento <span> .

<span class="field-validation-error" data-valmsg-for="Email"


data-valmsg-replace="true">
The Email Address field is required.
</span>

Aplicación auxiliar de etiquetas de resumen de validación


Tiene como destino todos los elementos <div> con el atributo asp-validation-summary .
Alternativa de aplicación auxiliar HTML: @Html.ValidationSummary .

Validation Summary Tag Helper se usa para mostrar un resumen de los mensajes de validación. El valor de atributo
asp-validation-summary puede ser cualquiera de los siguientes:

ASP-VALIDATION-SUMMARY MENSAJES DE VALIDACIÓN QUE SE MUESTRAN

ValidationSummary.All Nivel de modelo y de propiedad

ValidationSummary.ModelOnly Modelo

ValidationSummary.None Ninguna

Ejemplo
En el siguiente ejemplo, el modelo de datos se complementa con atributos DataAnnotation , lo que genera mensajes
de error de validación sobre el elemento <input> . Cuando se produce un error de validación, la aplicación auxiliar
de etiquetas de validación muestra el mensaje de error:

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }

[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
}

@model RegisterViewModel

<form asp-controller="Demo" asp-action="RegisterValidation" method="post">


<div asp-validation-summary="ModelOnly"></div>
Email: <input asp-for="Email" /> <br />
<span asp-validation-for="Email"></span><br />
Password: <input asp-for="Password" /><br />
<span asp-validation-for="Password"></span><br />
<button type="submit">Register</button>
</form>

El código HTML generado (cuando el modelo es válido) es este:


<form action="/DemoReg/Register" method="post">
<div class="validation-summary-valid" data-valmsg-summary="true">
<ul><li style="display:none"></li></ul></div>
Email: <input name="Email" id="Email" type="email" value=""
data-val-required="The Email field is required."
data-val-email="The Email field is not a valid email address."
data-val="true"> <br>
<span class="field-validation-valid" data-valmsg-replace="true"
data-valmsg-for="Email"></span><br>
Password: <input name="Password" id="Password" type="password"
data-val-required="The Password field is required." data-val="true"><br>
<span class="field-validation-valid" data-valmsg-replace="true"
data-valmsg-for="Password"></span><br>
<button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

Aplicación auxiliar de etiquetas de selección (Select)


Genera el elemento select y el elemento asociado option de las propiedades del modelo.
Tiene Html.DropDownListFor y Html.ListBoxFor como alternativa de aplicación auxiliar HTML.
El elemento asp-for de Select Tag Helper especifica el nombre de la propiedad de modelo del elemento select,
mientras que asp-items especifica los elementos option. Por ejemplo:

<select asp-for="Country" asp-items="Model.Countries"></select>

Ejemplo:

using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace FormsTagHelper.ViewModels
{
public class CountryViewModel
{
public string Country { get; set; }

public List<SelectListItem> Countries { get; } = new List<SelectListItem>


{
new SelectListItem { Value = "MX", Text = "Mexico" },
new SelectListItem { Value = "CA", Text = "Canada" },
new SelectListItem { Value = "US", Text = "USA" },
};
}
}

El método Index inicializa CountryViewModel , establece el país seleccionado y lo pasa a la vista Index .

public IActionResult IndexOption(int id)


{
var model = new CountryViewModel();
model.Country = "CA";
return View(model);
}

El método HTTP POST Index muestra la selección:


[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Index(CountryViewModel model)
{
if (ModelState.IsValid)
{
var msg = model.Country + " selected";
return RedirectToAction("IndexSuccess", new { message = msg});
}

// If we got this far, something failed; redisplay form.


return View(model);
}

La vista Index :

@model CountryViewModel

<form asp-controller="Home" asp-action="Index" method="post">


<select asp-for="Country" asp-items="Model.Countries"></select>
<br /><button type="submit">Register</button>
</form>

Genera el siguiente código HTML (con la selección "CA"):

<form method="post" action="/">


<select id="Country" name="Country">
<option value="MX">Mexico</option>
<option selected="selected" value="CA">Canada</option>
<option value="US">USA</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

NOTE
No se recomienda usar ViewBag o ViewData con la aplicación auxiliar de etiquetas Select. Un modelo de vista es más eficaz
a la hora de proporcionar metadatos MVC y suele ser menos problemático.

El valor de atributo asp-for es un caso especial y no necesita un prefijo Model , mientras que los otros atributos de
aplicación auxiliar de etiquetas sí (como asp-items ).

<select asp-for="Country" asp-items="Model.Countries"></select>

Enlace con enum


A menudo, conviene usar <select> con una propiedad enum y generar los elementos SelectListItem a partir de
valores enum .
Ejemplo:
public class CountryEnumViewModel
{
public CountryEnum EnumCountry { get; set; }
}
}

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public enum CountryEnum
{
[Display(Name = "United Mexican States")]
Mexico,
[Display(Name = "United States of America")]
USA,
Canada,
France,
Germany,
Spain
}
}

El método GetEnumSelectList genera un objeto SelectList para una enumeración.

@model CountryEnumViewModel

<form asp-controller="Home" asp-action="IndexEnum" method="post">


<select asp-for="EnumCountry"
asp-items="Html.GetEnumSelectList<CountryEnum>()"> >
</select>
<br /><button type="submit">Register</button>
</form>

Puede complementar la lista de enumeradores con el atributo Display para obtener una interfaz de usuario más
completa:

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public enum CountryEnum
{
[Display(Name = "United Mexican States")]
Mexico,
[Display(Name = "United States of America")]
USA,
Canada,
France,
Germany,
Spain
}
}

Se genera el siguiente código HTML:


<form method="post" action="/Home/IndexEnum">
<select data-val="true" data-val-required="The EnumCountry field is required."
id="EnumCountry" name="EnumCountry">
<option value="0">United Mexican States</option>
<option value="1">United States of America</option>
<option value="2">Canada</option>
<option value="3">France</option>
<option value="4">Germany</option>
<option selected="selected" value="5">Spain</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

Agrupamiento de opciones
El elemento HTML <optgroup> se genera cuando el modelo de vista contiene uno o varios objetos
SelectListGroup .

CountryViewModelGroup agrupa los elementos SelectListItem en los grupos "North America" y "Europe":
public class CountryViewModelGroup
{
public CountryViewModelGroup()
{
var NorthAmericaGroup = new SelectListGroup { Name = "North America" };
var EuropeGroup = new SelectListGroup { Name = "Europe" };

Countries = new List<SelectListItem>


{
new SelectListItem
{
Value = "MEX",
Text = "Mexico",
Group = NorthAmericaGroup
},
new SelectListItem
{
Value = "CAN",
Text = "Canada",
Group = NorthAmericaGroup
},
new SelectListItem
{
Value = "US",
Text = "USA",
Group = NorthAmericaGroup
},
new SelectListItem
{
Value = "FR",
Text = "France",
Group = EuropeGroup
},
new SelectListItem
{
Value = "ES",
Text = "Spain",
Group = EuropeGroup
},
new SelectListItem
{
Value = "DE",
Text = "Germany",
Group = EuropeGroup
}
};
}

public string Country { get; set; }

public List<SelectListItem> Countries { get; }

Aquí mostramos los dos grupos:


El código HTML generado:

<form method="post" action="/Home/IndexGroup">


<select id="Country" name="Country">
<optgroup label="North America">
<option value="MEX">Mexico</option>
<option value="CAN">Canada</option>
<option value="US">USA</option>
</optgroup>
<optgroup label="Europe">
<option value="FR">France</option>
<option value="ES">Spain</option>
<option value="DE">Germany</option>
</optgroup>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

Selección múltiple
La aplicación auxiliar de etiquetas Select generará automáticamente el atributo multiple = "multiple" si la propiedad
especificada en el atributo asp-for es IEnumerable . Por ejemplo, si tenemos el siguiente modelo:

using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace FormsTagHelper.ViewModels
{
public class CountryViewModelIEnumerable
{
public IEnumerable<string> CountryCodes { get; set; }

public List<SelectListItem> Countries { get; } = new List<SelectListItem>


{
new SelectListItem { Value = "MX", Text = "Mexico" },
new SelectListItem { Value = "CA", Text = "Canada" },
new SelectListItem { Value = "US", Text = "USA" },
new SelectListItem { Value = "FR", Text = "France" },
new SelectListItem { Value = "ES", Text = "Spain" },
new SelectListItem { Value = "DE", Text = "Germany"}
};
}
}

Con la siguiente vista:


@model CountryViewModelIEnumerable

<form asp-controller="Home" asp-action="IndexMultiSelect" method="post">


<select asp-for="CountryCodes" asp-items="Model.Countries"></select>
<br /><button type="submit">Register</button>
</form>

Se genera el siguiente código HTML:

<form method="post" action="/Home/IndexMultiSelect">


<select id="CountryCodes"
multiple="multiple"
name="CountryCodes"><option value="MX">Mexico</option>
<option value="CA">Canada</option>
<option value="US">USA</option>
<option value="FR">France</option>
<option value="ES">Spain</option>
<option value="DE">Germany</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

Sin selección
Si ve que usa la opción "sin especificar" en varias páginas, puede crear una plantilla para no tener que repetir el
código HTML:

@model CountryViewModel

<form asp-controller="Home" asp-action="IndexEmpty" method="post">


@Html.EditorForModel()
<br /><button type="submit">Register</button>
</form>

La plantilla Views/Shared/EditorTemplates/CountryViewModel.cshtml:

@model CountryViewModel

<select asp-for="Country" asp-items="Model.Countries">


<option value="">--none--</option>
</select>

La posibilidad de agregar elementos HTML <option> no se limita exclusivamente a los casos en los que no se
seleccionada nada. Por ejemplo, el método de acción y vista siguientes generarán un código HTML similar al
código anterior:

public IActionResult IndexOption(int id)


{
var model = new CountryViewModel();
model.Country = "CA";
return View(model);
}
@model CountryViewModel

<form asp-controller="Home" asp-action="IndexEmpty" method="post">


<select asp-for="Country">
<option value="">&lt;none&gt;</option>
<option value="MX">Mexico</option>
<option value="CA">Canada</option>
<option value="US">USA</option>
</select>
<br /><button type="submit">Register</button>
</form>

Se seleccionará el elemento <option> correcto (que contenga el atributo selected="selected" ) en función del valor
real de Country .

<form method="post" action="/Home/IndexEmpty">


<select id="Country" name="Country">
<option value="">&lt;none&gt;</option>
<option value="MX">Mexico</option>
<option value="CA" selected="selected">Canada</option>
<option value="US">USA</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>

Recursos adicionales
Aplicaciones auxiliares de etiquetas
HTML Form element (Elemento HTML Form)
Request Verification Token (Token de comprobación de solicitudes)
Enlace de modelos
Introduction to model validation in ASP.NET Core MVC (Introducción a la validación de modelos en ASP.NET
Core MVC )
IAttributeAdapter Interface (Interfaz IAttributeAdapter)
Fragmentos de código de este documento
Aplicaciones auxiliares de etiquetas en
formularios de ASP.NET Core
19/06/2018 • 29 minutes to read • Edit Online

Por Rick Anderson, Dave Paquette y Jerrie Pelser


En este documento se explica cómo trabajar con formularios y se detallan los
elementos HTML que se usan habitualmente en un formulario. El elemento HTML
Form proporciona el mecanismo principal que las aplicaciones web usan a la hora de
devolver datos al servidor. La mayor parte de este documento se centra en describir
las aplicaciones auxiliares de etiquetas y cómo pueden servir para crear formularios
HTML eficaces de manera productiva. Se recomienda leer Introduction to Tag Helpers
(Introducción a las aplicaciones auxiliares de etiquetas) antes de este documento.
En muchos casos, las aplicaciones auxiliares HTML proporcionan un método
alternativo a una aplicación auxiliar de etiquetas específica, pero es importante tener
en cuenta que las aplicaciones auxiliares de etiquetas no reemplazan a las aplicaciones
auxiliares HTML y que, de igual modo, no hay una aplicación auxiliar de etiqueta para
cada aplicación auxiliar HTML. Si existe una alternativa de aplicación auxiliar HTML,
se mencionará aquí.

Aplicación auxiliar de etiquetas de formulario (Form)


La aplicación auxiliar de etiquetas Form hace lo siguiente:
Genera el valor de atributo action del elemento HTML <FORM> de una
acción de controlador MVC o ruta con nombre.
Genera un token comprobación de solicitudes oculto que impide que se
falsifiquen solicitudes entre sitios (cuando se usa con el atributo
[ValidateAntiForgeryToken] en el método de acción HTTP Post).

Proporciona el atributo asp-route-<Parameter Name> , donde <Parameter Name>


se agrega a los valores de ruta. Los parámetros routeValues de
Html.BeginForm y Html.BeginRouteForm proporcionan una funcionalidad
similar.
Tiene Html.BeginForm y Html.BeginRouteForm como alternativa de aplicación
auxiliar HTML.
Ejemplo:

<form asp-controller="Demo" asp-action="Register" method="post">


<!-- Input and Submit elements -->
</form>

La aplicación auxiliar de etiquetas Form anterior genera el siguiente HTML:


<form method="post" action="/Demo/Register">
<!-- Input and Submit elements -->
<input name="__RequestVerificationToken" type="hidden" value="<removed for
brevity>" />
</form>

El tiempo de ejecución MVC genera el valor de atributo action de los atributos de


aplicación auxiliar de etiquetas Form asp-controller y asp-action . La aplicación
auxiliar de etiquetas Form genera también un token de comprobación de solicitudes
oculto que impide que se falsifiquen solicitudes entre sitios (cuando se usa con el
atributo [ValidateAntiForgeryToken] en el método de acción HTTP Post). Proteger un
elemento HTML Form puro de la falsificación de solicitudes entre sitios no es tarea
fácil, y la aplicación auxiliar de etiquetas Form presta este servicio.
Uso de una ruta con nombre
El atributo de aplicación auxiliar de etiquetas asp-route puede generar también el
marcado del atributo HTML action . Una aplicación con una ruta denominada
register podría usar el siguiente marcado para la página de registro:

<form asp-route="register" method="post">


<!-- Input and Submit elements -->
</form>

Muchas de las vistas de la carpeta Views/Account (que se genera cuando se crea una
aplicación web con Cuentas de usuario individuales) contienen el atributo asp-route-
returnurl:

<form asp-controller="Account" asp-action="Login"


asp-route-returnurl="@ViewData["ReturnUrl"]"
method="post" class="form-horizontal" role="form">

NOTE
Con las plantillas integradas, returnUrl se rellena automáticamente solo cuando alguien
intenta obtener acceso a un recurso autorizado, pero no se ha autenticado o no tiene
autorización. Si se intenta realizar un acceso no autorizado, el middleware de seguridad
redirige a la página de inicio de sesión con returnUrl configurado.

Aplicación auxiliar de etiquetas de entrada (Input)


La aplicación auxiliar de etiquetas Input enlaza un elemento HTML <input> a una
expresión de modelo en la vista de Razor.
Sintaxis:

<input asp-for="<Expression Name>" />

La aplicación auxiliar de etiquetas Input hace lo siguiente:


Genera los atributos HTML id y name del nombre de expresión especificado
en el atributo asp-for . asp-for="Property1.Property2" es equivalente a
m => m.Property1.Property2 . El nombre de una expresión es lo que se usa para
el valor de atributo asp-for . Vea la sección Nombres de expresión para
obtener más información.
Establece el valor de atributo HTML type según los atributos de tipo de
modelo y de anotación de datos aplicados a la propiedad de modelo.
No sobrescribirá el valor de atributo HTML type cuando se especifique uno.
Genera atributos de validación HTML5 a partir de los atributos de anotación
de datos aplicados a las propiedades de modelo.
Tiene características de aplicación auxiliar HTML que se superponen a
Html.TextBoxFor y Html.EditorFor . Vea la sección Alternativas de
aplicación auxiliar HTML a la aplicación auxiliar de etiquetas Input para
obtener más información.
Permite establecer tipado fuerte. Si el nombre de la propiedad cambia y no se
actualiza la aplicación auxiliar de etiquetas, aparecerá un error similar al
siguiente:

An error occurred during the compilation of a resource required to process


this request. Please review the following specific error details and modify
your source code appropriately.

Type expected
'RegisterViewModel' does not contain a definition for 'Email' and no
extension method 'Email' accepting a first argument of type 'RegisterViewModel'
could be found (are you missing a using directive or an assembly reference?)

La aplicación auxiliar de etiquetas Input establece el atributo HTML type en


función del tipo .NET. En la siguiente tabla se enumeran algunos tipos .NET habituales
y el tipo HTML generado correspondiente (no incluimos aquí todos los tipos .NET).

TIPO DE .NET TIPO DE ENTRADA

Bool type=”checkbox”

String type=”text”

DateTime type=”datetime”

Byte type=”number”

Valor int. type=”number”

Single, Double type=”number”

En la siguiente tabla se muestran algunos atributos de anotación de datos comunes


que la aplicación auxiliar de etiquetas Input asignará a tipos de entrada concretos (no
incluimos aquí todos los atributo de validación):

ATRIBUTO TIPO DE ENTRADA

[EmailAddress] type=”email”
ATRIBUTO TIPO DE ENTRADA

[Url] type=”url”

[HiddenInput] type=”hidden”

[Phone] type=”tel”

[DataType(DataType.Password)] type=”password”

[DataType(DataType.Date)] type=”date”

[DataType(DataType.Time)] type=”time”

Ejemplo:

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }

[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
}

@model RegisterViewModel

<form asp-controller="Demo" asp-action="RegisterInput" method="post">


Email: <input asp-for="Email" /> <br />
Password: <input asp-for="Password" /><br />
<button type="submit">Register</button>
</form>

El código anterior genera el siguiente HTML:

<form method="post" action="/Demo/RegisterInput">


Email:
<input type="email" data-val="true"
data-val-email="The Email Address field is not a valid email
address."
data-val-required="The Email Address field is required."
id="Email" name="Email" value="" /> <br>
Password:
<input type="password" data-val="true"
data-val-required="The Password field is required."
id="Password" name="Password" /><br>
<button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for
brevity>" />
</form>
Las anotaciones de datos que se aplican a las propiedades Email y Password
generan metadatos en el modelo. La aplicación auxiliar de etiquetas Input usa esos
metadatos del modelo y genera atributos HTML5 data-val-* . Vea Model Validation
(Validación del modelo). Estos atributos describen los validadores que se van a
adjuntar a los campos de entrada, lo que proporciona HTML5 discreto y validación de
jQuery. Los atributos discretos tienen el formato data-val-rule="Error Message" ,
donde "rule" es el nombre de la regla de validación (como data-val-required ,
data-val-email , data-val-maxlength , etc.). Si aparece un mensaje de error en el
atributo, se mostrará como el valor del atributo data-val-rule . También hay atributos
con el formato data-val-ruleName-argumentName="argumentValue" que aportan más
información sobre la regla, por ejemplo, data-val-maxlength-max="1024" .
Alternativas de aplicación auxiliar HTML a la aplicación auxiliar de etiquetas Input
Html.TextBox , Html.TextBoxFor , Html.Editor y Html.EditorFor tienen características
que se superponen a la aplicación auxiliar de etiquetas Input. La aplicación auxiliar de
etiquetas Input establecerá automáticamente el atributo type , cosa que no ocurrirá
con Html.TextBox ni Html.TextBoxFor . Html.Editor y Html.EditorFor controlan
colecciones, objetos complejos y plantillas, pero la aplicación auxiliar de etiquetas
Input no. La aplicación auxiliar de etiquetas Input, Html.EditorFor y Html.TextBoxFor
están fuertemente tipados (usan expresiones lambda), pero Html.TextBox y
Html.Editor no (usan nombres de expresión).

HtmlAttributes
@Html.Editor() y @Html.EditorFor()usan una entrada ViewDataDictionary especial
denominada htmlAttributes al ejecutar sus plantillas predeterminadas. Si lo desea,
este comportamiento se puede enriquecer con parámetros additionalViewData . En la
clave "htmlAttributes" se distingue entre mayúsculas y minúsculas. La clave
"htmlAttributes" se controla de forma similar al objeto htmlAttributes pasado a
aplicaciones auxiliares de etiqueta Input como @Html.TextBox() .

@Html.EditorFor(model => model.YourProperty,


new { htmlAttributes = new { @class="myCssClass", style="Width:100px" } })

Nombres de expresión
El valor del atributo asp-for es una ModelExpression y la parte de la derecha de una
expresión lambda. Por tanto, asp-for="Property1" se convierte en m => m.Property1
en el código generado, motivo por el que no es necesario incluir el prefijo Model .
Puede usar el carácter "@" para iniciar una expresión insertada y moverla antes de
`m.`:</span><span class="sxs-lookup">You can use the "@" character to start an
inline expression and move before the m. :

@{
var joe = "Joe";
}
<input asp-for="@joe" />

Se genera el siguiente HTML:

<input type="text" id="joe" name="joe" value="Joe" />

Con las propiedades de colección, asp-for="CollectionProperty[23].Member" genera el


mismo nombre que asp-for="CollectionProperty[i].Member" si i tiene el valor 23 .
Cuando ASP.NET Core MVC calcula el valor de ModelExpression , inspecciona varios
orígenes, ModelState incluido. Fíjese en <input type="text" asp-for="@Name" /> . El
atributo value calculado es el primer valor distinto de null de:
La entrada ModelState con la clave "Name".
El resultado de la expresión Model.Name .
Navegar a las propiedades secundarias
También se puede navegar a las propiedades secundarias a través de la ruta de acceso
de propiedades del modelo de vista. Pensemos en una clase de modelo más compleja
que contiene una propiedad secundaria Address .

public class AddressViewModel


{
public string AddressLine1 { get; set; }
}

public class RegisterAddressViewModel


{
public string Email { get; set; }

[DataType(DataType.Password)]
public string Password { get; set; }

public AddressViewModel Address { get; set; }


}

En la vista, enlazamos a Address.AddressLine1 :

@model RegisterAddressViewModel

<form asp-controller="Demo" asp-action="RegisterAddress" method="post">


Email: <input asp-for="Email" /> <br />
Password: <input asp-for="Password" /><br />
Address: <input asp-for="Address.AddressLine1" /><br />
<button type="submit">Register</button>
</form>

Se genera el siguiente código HTML para Address.AddressLine1 :

<input type="text" id="Address_AddressLine1" name="Address.AddressLine1" value=""


/>

Colecciones y nombres de expresión


Como ejemplo, un modelo que contiene una matriz de Colors :

public class Person


{
public List<string> Colors { get; set; }

public int Age { get; set; }


}

El método de acción:
public IActionResult Edit(int id, int colorIndex)
{
ViewData["Index"] = colorIndex;
return View(GetPerson(id));
}

El siguiente código de Razor muestra cómo se tiene acceso a un elemento Color


concreto:

@model Person
@{
var index = (int)ViewData["index"];
}

<form asp-controller="ToDo" asp-action="Edit" method="post">


@Html.EditorFor(m => m.Colors[index])
<label asp-for="Age"></label>
<input asp-for="Age" /><br />
<button type="submit">Post</button>
</form>

La plantilla Views/Shared/EditorTemplates/String.cshtml:

@model string

<label asp-for="@Model"></label>
<input asp-for="@Model" /> <br />

Ejemplo en el que se usa List<T> :

public class ToDoItem


{
public string Name { get; set; }

public bool IsDone { get; set; }


}

El siguiente código de Razor muestra cómo iterar por una colección:

@model List<ToDoItem>

<form asp-controller="ToDo" asp-action="Edit" method="post">


<table>
<tr> <th>Name</th> <th>Is Done</th> </tr>

@for (int i = 0; i < Model.Count; i++)


{
<tr>
@Html.EditorFor(model => model[i])
</tr>
}

</table>
<button type="submit">Save</button>
</form>

La plantilla Views/Shared/EditorTemplates/ToDoItem.cshtml:
@model ToDoItem

<td>
<label asp-for="@Model.Name"></label>
@Html.DisplayFor(model => model.Name)
</td>
<td>
<input asp-for="@Model.IsDone" />
</td>

@*
This template replaces the following Razor which evaluates the indexer three
times.
<td>
<label asp-for="@Model[i].Name"></label>
@Html.DisplayFor(model => model[i].Name)
</td>
<td>
<input asp-for="@Model[i].IsDone" />
</td>
*@

NOTE
Use siempre for (y no foreach ) para iterar por una lista. Evaluar un indizador en una
expresión de LINQ puede ser costoso, con lo cual esa posibilidad hay que reducirla al
mínimo.

NOTE
El código de ejemplo comentado anterior muestra cómo reemplazaríamos la expresión
lambda por el operador @ para tener acceso a cada elemento ToDoItem de la lista.

Aplicación auxiliar de etiquetas de área de texto


(Textarea)
La aplicación auxiliar de etiquetas Textarea Tag Helper es similar a la aplicación
auxiliar de etiquetas Input.
Genera los atributos id y name , y los atributos de validación de datos del
modelo de un elemento <textarea>.
Permite establecer tipado fuerte.
Alternativa de aplicación auxiliar HTML: Html.TextAreaFor .

Ejemplo:
using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class DescriptionViewModel
{
[MinLength(5)]
[MaxLength(1024)]
public string Description { get; set; }
}
}

@model DescriptionViewModel

<form asp-controller="Demo" asp-action="RegisterTextArea" method="post">


<textarea asp-for="Description"></textarea>
<button type="submit">Test</button>
</form>

Se genera el siguiente código HTML:

<form method="post" action="/Demo/RegisterTextArea">


<textarea data-val="true"
data-val-maxlength="The field Description must be a string or array type with a
maximum length of &#x27;1024&#x27;."
data-val-maxlength-max="1024"
data-val-minlength="The field Description must be a string or array type with a
minimum length of &#x27;5&#x27;."
data-val-minlength-min="5"
id="Description" name="Description">
</textarea>
<button type="submit">Test</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for
brevity>" />
</form>

Aplicación auxiliar de etiquetas Label


Genera el título de la etiqueta y el atributo for en un elemento de un nombre
de expresión.
Alternativa de aplicación auxiliar HTML: Html.LabelFor .

Label Tag Helper proporciona las siguientes ventajas con respecto a un elemento
HTML Label puro:
Obtendrá automáticamente el valor de la etiqueta descriptiva del atributo
Display . El nombre para mostrar que se busca puede cambiar con el tiempo y
la combinación del atributo Display , y la aplicación auxiliar de etiquetas Label
aplicará el elemento Display en cualquier lugar donde se use.
El código fuente contiene menos marcado.
Permite establecer tipado fuerte con la propiedad de modelo.
Ejemplo:
using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class SimpleViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }
}
}

@model SimpleViewModel

<form asp-controller="Demo" asp-action="RegisterLabel" method="post">


<label asp-for="Email"></label>
<input asp-for="Email" /> <br />
</form>

Se genera el siguiente código HTML para el elemento <label> :

<label for="Email">Email Address</label>

La aplicación auxiliar de etiquetas Label genera el valor de atributo for de "Email",


que es el identificador asociado al elemento <input> . Las aplicaciones auxiliares de
etiqueta generan elementos id y for coherentes para que se puedan asociar
correctamente. El título de este ejemplo proviene del atributo Display . Si el modelo
no contuviera un atributo Display , el título sería el nombre de propiedad de la
expresión.

Aplicaciones auxiliares de etiquetas de validación


Hay dos aplicaciones auxiliares de etiquetas de validación:
Validation Message Tag Helper (que muestra un mensaje de validación relativo a una
única propiedad del modelo) y Validation Summary Tag Helper (que muestra un
resumen de los errores de validación). Input Tag Helper agrega atributos de
validación del lado cliente HTML5 a los elementos de entrada en función de los
atributos de anotación de datos de las clases del modelo. La validación también se
realiza en el lado servidor. La aplicación auxiliar de etiquetas de validación muestra
estos mensajes de error cuando se produce un error de validación.
Aplicación auxiliar de etiquetas de mensaje de validación
Agrega el atributo HTML5 data-valmsg-for="property" al elemento span, que
adjunta los mensajes de error de validación en el campo de entrada de la
propiedad de modelo especificada. Cuando se produce un error de validación
en el lado cliente, jQuery muestra el mensaje de error en el elemento <span> .
La validación también tiene lugar en el lado servidor. Puede que JavaScript
esté deshabilitado en los clientes, mientras que hay algunas validaciones que
solo se pueden realizar en el lado servidor.
Alternativa de aplicación auxiliar HTML: Html.ValidationMessageFor .
Validation Message Tag Helper se usa con el atributo asp-validation-for en un
elemento HTML span.

<span asp-validation-for="Email"></span>

La aplicación auxiliar de etiquetas de mensaje de validación generará el siguiente


código HTML:

<span class="field-validation-valid"
data-valmsg-for="Email"
data-valmsg-replace="true"></span>

Validation Message Tag Helper suele usarse por lo general después de una aplicación
auxiliar de etiquetas Input en la misma propiedad. Gracias a esto, se mostrarán
todos los mensajes de error de validación cerca de la entrada que produjo el error.

NOTE
Debe tener una vista con las referencias de script de JavaScript y de jQuery adecuadas para
la validación del lado cliente. Para más información, vea Introduction to model validation in
ASP.NET Core MVC (Introducción a la validación de modelos en ASP.NET Core MVC).

Cuando se produce un error de validación del lado servidor (por ejemplo, porque
haya una validación del lado servidor personalizada o porque la validación del lado
cliente esté deshabilitada), MVC pone ese mensaje de error como cuerpo del
elemento <span> .

<span class="field-validation-error" data-valmsg-for="Email"


data-valmsg-replace="true">
The Email Address field is required.
</span>

Aplicación auxiliar de etiquetas de resumen de validación


Tiene como destino todos los elementos <div> con el atributo
asp-validation-summary .

Alternativa de aplicación auxiliar HTML: @Html.ValidationSummary .

Validation Summary Tag Helper se usa para mostrar un resumen de los mensajes de
validación. El valor de atributo asp-validation-summary puede ser cualquiera de los
siguientes:

ASP-VALIDATION-SUMMARY MENSAJES DE VALIDACIÓN QUE SE MUESTRAN

ValidationSummary.All Nivel de modelo y de propiedad

ValidationSummary.ModelOnly Modelo

ValidationSummary.None Ninguna

Ejemplo
En el siguiente ejemplo, el modelo de datos se complementa con atributos
DataAnnotation , lo que genera mensajes de error de validación sobre el elemento
<input> . Cuando se produce un error de validación, la aplicación auxiliar de etiquetas
de validación muestra el mensaje de error:

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }

[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
}

@model RegisterViewModel

<form asp-controller="Demo" asp-action="RegisterValidation" method="post">


<div asp-validation-summary="ModelOnly"></div>
Email: <input asp-for="Email" /> <br />
<span asp-validation-for="Email"></span><br />
Password: <input asp-for="Password" /><br />
<span asp-validation-for="Password"></span><br />
<button type="submit">Register</button>
</form>

El código HTML generado (cuando el modelo es válido) es este:

<form action="/DemoReg/Register" method="post">


<div class="validation-summary-valid" data-valmsg-summary="true">
<ul><li style="display:none"></li></ul></div>
Email: <input name="Email" id="Email" type="email" value=""
data-val-required="The Email field is required."
data-val-email="The Email field is not a valid email address."
data-val="true"> <br>
<span class="field-validation-valid" data-valmsg-replace="true"
data-valmsg-for="Email"></span><br>
Password: <input name="Password" id="Password" type="password"
data-val-required="The Password field is required." data-val="true"><br>
<span class="field-validation-valid" data-valmsg-replace="true"
data-valmsg-for="Password"></span><br>
<button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for
brevity>" />
</form>

Aplicación auxiliar de etiquetas de selección (Select)


Genera el elemento select y el elemento asociado option de las propiedades
del modelo.
Tiene Html.DropDownListFor y Html.ListBoxFor como alternativa de aplicación
auxiliar HTML.
El elemento asp-for de Select Tag Helper especifica el nombre de la propiedad de
modelo del elemento select, mientras que asp-items especifica los elementos option.
Por ejemplo:

<select asp-for="Country" asp-items="Model.Countries"></select>

Ejemplo:

using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace FormsTagHelper.ViewModels
{
public class CountryViewModel
{
public string Country { get; set; }

public List<SelectListItem> Countries { get; } = new List<SelectListItem>


{
new SelectListItem { Value = "MX", Text = "Mexico" },
new SelectListItem { Value = "CA", Text = "Canada" },
new SelectListItem { Value = "US", Text = "USA" },
};
}
}

El método Index inicializa CountryViewModel , establece el país seleccionado y lo pasa


a la vista Index .

public IActionResult IndexOption(int id)


{
var model = new CountryViewModel();
model.Country = "CA";
return View(model);
}

El método HTTP POST Index muestra la selección:

[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Index(CountryViewModel model)
{
if (ModelState.IsValid)
{
var msg = model.Country + " selected";
return RedirectToAction("IndexSuccess", new { message = msg});
}

// If we got this far, something failed; redisplay form.


return View(model);
}

La vista Index :
@model CountryViewModel

<form asp-controller="Home" asp-action="Index" method="post">


<select asp-for="Country" asp-items="Model.Countries"></select>
<br /><button type="submit">Register</button>
</form>

Genera el siguiente código HTML (con la selección "CA"):

<form method="post" action="/">


<select id="Country" name="Country">
<option value="MX">Mexico</option>
<option selected="selected" value="CA">Canada</option>
<option value="US">USA</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for
brevity>" />
</form>

NOTE
No se recomienda usar ViewBag o ViewData con la aplicación auxiliar de etiquetas Select.
Un modelo de vista es más eficaz a la hora de proporcionar metadatos MVC y suele ser
menos problemático.

El valor de atributo asp-for es un caso especial y no necesita un prefijo Model ,


mientras que los otros atributos de aplicación auxiliar de etiquetas sí (como
asp-items ).

<select asp-for="Country" asp-items="Model.Countries"></select>

Enlace con enum


A menudo, conviene usar <select> con una propiedad enum y generar los
elementos SelectListItem a partir de valores enum .
Ejemplo:

public class CountryEnumViewModel


{
public CountryEnum EnumCountry { get; set; }
}
}
using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public enum CountryEnum
{
[Display(Name = "United Mexican States")]
Mexico,
[Display(Name = "United States of America")]
USA,
Canada,
France,
Germany,
Spain
}
}

El método GetEnumSelectList genera un objeto SelectList para una enumeración.

@model CountryEnumViewModel

<form asp-controller="Home" asp-action="IndexEnum" method="post">


<select asp-for="EnumCountry"
asp-items="Html.GetEnumSelectList<CountryEnum>()"> >
</select>
<br /><button type="submit">Register</button>
</form>

Puede complementar la lista de enumeradores con el atributo Display para obtener


una interfaz de usuario más completa:

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public enum CountryEnum
{
[Display(Name = "United Mexican States")]
Mexico,
[Display(Name = "United States of America")]
USA,
Canada,
France,
Germany,
Spain
}
}

Se genera el siguiente código HTML:


<form method="post" action="/Home/IndexEnum">
<select data-val="true" data-val-required="The EnumCountry field is
required."
id="EnumCountry" name="EnumCountry">
<option value="0">United Mexican States</option>
<option value="1">United States of America</option>
<option value="2">Canada</option>
<option value="3">France</option>
<option value="4">Germany</option>
<option selected="selected" value="5">Spain</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed
for brevity>" />
</form>

Agrupamiento de opciones
El elemento HTML <optgroup> se genera cuando el modelo de vista contiene uno o
varios objetos SelectListGroup .
CountryViewModelGroup agrupa los elementos SelectListItem en los grupos "North
America" y "Europe":
public class CountryViewModelGroup
{
public CountryViewModelGroup()
{
var NorthAmericaGroup = new SelectListGroup { Name = "North America" };
var EuropeGroup = new SelectListGroup { Name = "Europe" };

Countries = new List<SelectListItem>


{
new SelectListItem
{
Value = "MEX",
Text = "Mexico",
Group = NorthAmericaGroup
},
new SelectListItem
{
Value = "CAN",
Text = "Canada",
Group = NorthAmericaGroup
},
new SelectListItem
{
Value = "US",
Text = "USA",
Group = NorthAmericaGroup
},
new SelectListItem
{
Value = "FR",
Text = "France",
Group = EuropeGroup
},
new SelectListItem
{
Value = "ES",
Text = "Spain",
Group = EuropeGroup
},
new SelectListItem
{
Value = "DE",
Text = "Germany",
Group = EuropeGroup
}
};
}

public string Country { get; set; }

public List<SelectListItem> Countries { get; }

Aquí mostramos los dos grupos:


El código HTML generado:

<form method="post" action="/Home/IndexGroup">


<select id="Country" name="Country">
<optgroup label="North America">
<option value="MEX">Mexico</option>
<option value="CAN">Canada</option>
<option value="US">USA</option>
</optgroup>
<optgroup label="Europe">
<option value="FR">France</option>
<option value="ES">Spain</option>
<option value="DE">Germany</option>
</optgroup>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for
brevity>" />
</form>

Selección múltiple
La aplicación auxiliar de etiquetas Select generará automáticamente el atributo
multiple = "multiple" si la propiedad especificada en el atributo asp-for es
IEnumerable . Por ejemplo, si tenemos el siguiente modelo:
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace FormsTagHelper.ViewModels
{
public class CountryViewModelIEnumerable
{
public IEnumerable<string> CountryCodes { get; set; }

public List<SelectListItem> Countries { get; } = new List<SelectListItem>


{
new SelectListItem { Value = "MX", Text = "Mexico" },
new SelectListItem { Value = "CA", Text = "Canada" },
new SelectListItem { Value = "US", Text = "USA" },
new SelectListItem { Value = "FR", Text = "France" },
new SelectListItem { Value = "ES", Text = "Spain" },
new SelectListItem { Value = "DE", Text = "Germany"}
};
}
}

Con la siguiente vista:

@model CountryViewModelIEnumerable

<form asp-controller="Home" asp-action="IndexMultiSelect" method="post">


<select asp-for="CountryCodes" asp-items="Model.Countries"></select>
<br /><button type="submit">Register</button>
</form>

Se genera el siguiente código HTML:

<form method="post" action="/Home/IndexMultiSelect">


<select id="CountryCodes"
multiple="multiple"
name="CountryCodes"><option value="MX">Mexico</option>
<option value="CA">Canada</option>
<option value="US">USA</option>
<option value="FR">France</option>
<option value="ES">Spain</option>
<option value="DE">Germany</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for
brevity>" />
</form>

Sin selección
Si ve que usa la opción "sin especificar" en varias páginas, puede crear una plantilla
para no tener que repetir el código HTML:

@model CountryViewModel

<form asp-controller="Home" asp-action="IndexEmpty" method="post">


@Html.EditorForModel()
<br /><button type="submit">Register</button>
</form>

La plantilla Views/Shared/EditorTemplates/CountryViewModel.cshtml:
@model CountryViewModel

<select asp-for="Country" asp-items="Model.Countries">


<option value="">--none--</option>
</select>

La posibilidad de agregar elementos HTML <option> no se limita exclusivamente a


los casos en los que no se seleccionada nada. Por ejemplo, el método de acción y vista
siguientes generarán un código HTML similar al código anterior:

public IActionResult IndexOption(int id)


{
var model = new CountryViewModel();
model.Country = "CA";
return View(model);
}

@model CountryViewModel

<form asp-controller="Home" asp-action="IndexEmpty" method="post">


<select asp-for="Country">
<option value="">&lt;none&gt;</option>
<option value="MX">Mexico</option>
<option value="CA">Canada</option>
<option value="US">USA</option>
</select>
<br /><button type="submit">Register</button>
</form>

Se seleccionará el elemento <option> correcto (que contenga el atributo


selected="selected" ) en función del valor real de Country .

<form method="post" action="/Home/IndexEmpty">


<select id="Country" name="Country">
<option value="">&lt;none&gt;</option>
<option value="MX">Mexico</option>
<option value="CA" selected="selected">Canada</option>
<option value="US">USA</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for
brevity>" />
</form>

Recursos adicionales
Aplicaciones auxiliares de etiquetas
HTML Form element (Elemento HTML Form)
Request Verification Token (Token de comprobación de solicitudes)
Enlace de modelos
Introduction to model validation in ASP.NET Core MVC (Introducción a la
validación de modelos en ASP.NET Core MVC )
IAttributeAdapter Interface (Interfaz IAttributeAdapter)
Fragmentos de código de este documento
Vistas parciales en ASP.NET Core
25/06/2018 • 9 minutes to read • Edit Online

Por Steve Smith, Maher JENDOUBI, Rick Anderson y Scott Sauber


ASP.NET Core MVC admite vistas parciales, las cuales resultan útiles cuando tiene partes reutilizables de
páginas web que quiere compartir entre distintas vistas.
Vea o descargue el código de ejemplo (cómo descargarlo)

¿Qué son las vistas parciales?


Una vista parcial es una vista que se representa dentro de otra vista. El HTML generado al ejecutar la vista
parcial se representa en la vista que realiza la llamada (o principal). Al igual que las vistas, las vistas parciales
usan la extensión de archivo .cshtml.

¿Cuándo debo usar vistas parciales?


Las vistas parciales son una forma eficaz de dividir vistas de gran tamaño en componentes más pequeños.
Pueden reducir la duplicación del contenido de la vista y permitir reutilizar los elementos de la vista. Se deben
especificar elementos de diseño comunes en _Layout.cshtml. El contenido reutilizable que no es de diseño se
puede encapsular en las vistas parciales.
Si tiene una página compleja formada por varias partes lógicas, puede ser útil trabajar con cada parte como su
propia vista parcial. Cada parte de la página puede visualizarse de forma aislada del resto de la página y la
vista para la propia página se vuelve mucho más sencilla, ya que solo contiene la estructura general de la
página y las llamadas para representar las vistas parciales.
Sugerencia: siga el principio Una vez y solo una (DRY ) en las vistas.

Declarar vistas parciales


Las vistas parciales se crean como cualquier otra vista: se crea un archivo .cshtml dentro de la carpeta Views.
No hay ninguna diferencia semántica entre una vista parcial y una vista normal: simplemente se representan
de forma diferente. Puede hacer que una vista se devuelva directamente desde el ViewResult de un
controlador y puede usar la misma vista como una vista parcial. La diferencia principal entre el modo en que
se representa una vista y una vista parcial es que las vistas parciales no ejecutan _ViewStart.cshtml, mientras
que las vistas normales sí (más información sobre _ViewStart.cshtml en Diseño).

Referencia a una vista parcial


Desde una página de vista, hay varias maneras de representar una vista parcial. El procedimiento
recomendado consiste en usar Html.PartialAsync , que devuelve un IHtmlString y se puede hacer referencia a
ella agregando un prefijo a la llamada con @ :

@await Html.PartialAsync("AuthorPartial")

Puede representar una vista parcial con RenderPartialAsync . Este método no devuelve ningún resultado;
transmite por secuencias la salida representada directamente a la respuesta. Como no devuelve ningún
resultado, debe llamarse desde un bloque de código de Razor:
@{
await Html.RenderPartialAsync("AuthorPartial");
}

Dado que transmite por secuencias el resultado directamente, RenderPartialAsync puede funcionar mejor en
algunos escenarios, pero se recomienda usar PartialAsync .
Aunque hay equivalentes sincrónicos de Html.PartialAsync ( Html.Partial ) y de Html.RenderPartialAsync (
Html.RenderPartial ), su uso no es aconsejable, ya que hay escenarios donde producen interbloqueos. Los
métodos sincrónicos dejarán de estar disponibles en futuras versiones.

NOTE
Si las vistas necesitan ejecutar código, el patrón recomendado es usar un componente de vista en lugar de una vista
parcial.

Detección de vistas parciales


Cuando se hace referencia a una vista parcial, se puede hacer referencia a su ubicación de varias maneras:

// Uses a view in current folder with this name


// If none is found, searches the Shared folder
@await Html.PartialAsync("ViewName")

// A view with this name must be in the same folder


@await Html.PartialAsync("ViewName.cshtml")

// Locate the view based on the application root


// Paths that start with "/" or "~/" refer to the application root
@await Html.PartialAsync("~/Views/Folder/ViewName.cshtml")
@await Html.PartialAsync("/Views/Folder/ViewName.cshtml")

// Locate the view using relative paths


@await Html.PartialAsync("../Account/LoginPartial.cshtml")

Puede tener diferentes vistas parciales con el mismo nombre en distintas carpetas de vistas. Cuando se hace
referencia a las vistas por su nombre (sin extensión de archivo), las vistas de cada carpeta usarán la vista
parcial en la misma carpeta que ellas. También puede especificar una vista parcial predeterminada que vaya a
usar, colocándola en la carpeta Shared. Cualquier vista que no tenga su propia versión de la vista parcial usará
la vista parcial compartida. Puede tener una vista parcial predeterminada (en Shared), que se haya
reemplazado por una vista parcial con el mismo nombre y en la misma carpeta que la vista principal.
Las vistas parciales se pueden encadenar. Es decir, una vista parcial puede llamar a otra vista parcial (siempre
que no creen un bucle). Dentro de cada vista o vista parcial, las rutas de acceso relativas son siempre relativas
con respecto a esa vista, no con respecto a la vista principal o de raíz.

NOTE
Si declara un section de Razor en una vista parcial, no estará visible para sus vistas principales, sino que se limitará a la
vista parcial.

Acceso a datos desde vistas parciales


Cuando se crea una instancia de una vista parcial, obtiene una copia del diccionario de ViewData de la vista
principal. Las actualizaciones realizadas en los datos dentro de la vista parcial no se conservan en la vista
principal. ViewData cambiado en una parcial vista se pierde cuando se devuelve la vista parcial.
Puede pasar una instancia de ViewDataDictionary a la vista parcial:

@await Html.PartialAsync("PartialName", customViewData)

También puede pasar un modelo a una vista parcial. Puede tratarse del modelo de vista de la página o un
objeto personalizado. Puede pasar un modelo a PartialAsync o a RenderPartialAsync :

@await Html.PartialAsync("PartialName", viewModel)

Puede pasar una instancia de ViewDataDictionary y un modelo de vista a una vista parcial:

@await Html.PartialAsync("ArticleSection", section,


new ViewDataDictionary(this.ViewData) { { "index", index } })

El marcado siguiente muestra la vista Views/Articles/Read.cshtml que contiene dos vistas parciales. La segunda
vista parcial se pasa a un modelo y ViewData a la vista parcial. Puede pasar un nuevo diccionario de ViewData
a la vez que conserva el ViewData existente si usa la sobrecarga del constructor de ViewDataDictionary
resaltado aquí abajo:

@using Microsoft.AspNetCore.Mvc.ViewFeatures
@using PartialViewsSample.ViewModels
@model Article

<h2>@Model.Title</h2>
@*Pass the authors name to Views\Shared\AuthorPartial.cshtml*@
@await Html.PartialAsync("AuthorPartial", Model.AuthorName)
@Model.PublicationDate

@*Loop over the Sections and pass in a section and additional ViewData
to the strongly typed Views\Articles\ArticleSection.cshtml partial view.*@
@{ var index = 0;
@foreach (var section in Model.Sections)
{
@await Html.PartialAsync("ArticleSection", section,
new ViewDataDictionary(this.ViewData) { { "index", index } })
index++;
}
}

Views/Shared/AuthorPartial:

@model string
<div>
<h3>@Model</h3>
This partial view came from /Views/Shared/AuthorPartial.cshtml.<br />
</div>

La vista parcial ArticleSection:


@using PartialViewsSample.ViewModels
@model ArticleSection

<h3>@Model.Title Index: @ViewData["index"] </h3>


<div>
@Model.Content
</div>

En tiempo de ejecución, las vistas parciales se representan en la vista principal, que a su vez se representa
dentro de _Layout.cshtml compartido.
Inserción de dependencias en vistas de ASP.NET
Core
14/05/2018 • 7 minutes to read • Edit Online

Por Steve Smith


ASP.NET Core admite la inserción de dependencias en vistas. Esto puede ser útil para servicios específicos de
vistas, como la localización o los datos necesarios solamente para rellenar los elementos de vistas. Debe intentar
mantener la separación de intereses entre los controladores y las vistas. La mayoría de los datos que muestran
las vistas deben pasarse desde el controlador.
Vea o descargue el código de ejemplo (cómo descargarlo)

Ejemplo sencillo
Puede insertar un servicio en una vista mediante la directiva @inject . Puede pensar que @inject es como si
agregara una propiedad a la vista y rellenara la propiedad mediante DI.
La sintaxis de @inject : @inject <type> <name>

Un ejemplo de @inject en acción:


@using System.Threading.Tasks
@using ViewInjectSample.Model
@using ViewInjectSample.Model.Services
@model IEnumerable<ToDoItem>
@inject StatisticsService StatsService
<!DOCTYPE html>
<html>
<head>
<title>To Do Items</title>
</head>
<body>
<div>
<h1>To Do Items</h1>
<ul>
<li>Total Items: @StatsService.GetCount()</li>
<li>Completed: @StatsService.GetCompletedCount()</li>
<li>Avg. Priority: @StatsService.GetAveragePriority()</li>
</ul>
<table>
<tr>
<th>Name</th>
<th>Priority</th>
<th>Is Done?</th>
</tr>
@foreach (var item in Model)
{
<tr>
<td>@item.Name</td>
<td>@item.Priority</td>
<td>@item.IsDone</td>
</tr>
}
</table>
</div>
</body>
</html>

Esta vista muestra una lista de instancias ToDoItem , junto con un resumen de estadísticas generales. El resumen
se rellena a partir de StatisticsService insertado. Este servicio está registrado para la inserción de
dependencias en ConfigureServices en Startup.cs:

// For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?


LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();

services.AddTransient<IToDoItemRepository, ToDoItemRepository>();
services.AddTransient<StatisticsService>();
services.AddTransient<ProfileOptionsService>();

StatisticsService realiza algunos cálculos en el conjunto de instancias de ToDoItem , al que se accede a través
de un repositorio:
using System.Linq;
using ViewInjectSample.Interfaces;

namespace ViewInjectSample.Model.Services
{
public class StatisticsService
{
private readonly IToDoItemRepository _toDoItemRepository;

public StatisticsService(IToDoItemRepository toDoItemRepository)


{
_toDoItemRepository = toDoItemRepository;
}

public int GetCount()


{
return _toDoItemRepository.List().Count();
}

public int GetCompletedCount()


{
return _toDoItemRepository.List().Count(x => x.IsDone);
}

public double GetAveragePriority()


{
if (_toDoItemRepository.List().Count() == 0)
{
return 0.0;
}

return _toDoItemRepository.List().Average(x => x.Priority);


}
}
}

El repositorio de ejemplo usa una colección en memoria. La implementación que se muestra arriba (que
funciona en todos los datos en memoria) no se recomienda para conjuntos de datos grandes, con acceso de
forma remota.
El ejemplo muestra los datos del modelo enlazado a la vista y del servicio que se inserta en la vista:

Rellenar datos de búsqueda


La inserción de vistas puede ser útil para rellenar opciones en elementos de interfaz de usuario, como por
ejemplo, listas desplegables. Imagine un formulario de perfil de usuario que incluye opciones para especificar el
sexo, el estado y otras preferencias. Para representar este tipo de formulario mediante un enfoque MVC
estándar, necesitaría que el controlador solicitara servicios de acceso a datos para cada uno de estos conjuntos
de opciones y, después, rellenar un modelo o ViewBag con cada conjunto de opciones para enlazar.
Un enfoque alternativo consiste en insertar servicios directamente en la vista para obtener las opciones. Esto
reduce la cantidad de código necesario para el controlador, ya que mueve esta lógica de construcción del
elemento de vista a la propia vista. La acción del controlador para mostrar un formulario de edición de perfil
solamente necesita pasar el formulario de la instancia de perfil:

using Microsoft.AspNetCore.Mvc;
using ViewInjectSample.Model;

namespace ViewInjectSample.Controllers
{
public class ProfileController : Controller
{
[Route("Profile")]
public IActionResult Index()
{
// TODO: look up profile based on logged-in user
var profile = new Profile()
{
Name = "Steve",
FavColor = "Blue",
Gender = "Male",
State = new State("Ohio","OH")
};
return View(profile);
}
}
}

El formulario HTML que se utilizó para actualizar estas preferencias incluye listas desplegables para tres de las
propiedades:

Estas listas se rellenan mediante un servicio que se ha insertado en la vista:


@using System.Threading.Tasks
@using ViewInjectSample.Model.Services
@model ViewInjectSample.Model.Profile
@inject ProfileOptionsService Options
<!DOCTYPE html>
<html>
<head>
<title>Update Profile</title>
</head>
<body>
<div>
<h1>Update Profile</h1>
Name: @Html.TextBoxFor(m => m.Name)
<br/>
Gender: @Html.DropDownList("Gender",
Options.ListGenders().Select(g =>
new SelectListItem() { Text = g, Value = g }))
<br/>

State: @Html.DropDownListFor(m => m.State.Code,


Options.ListStates().Select(s =>
new SelectListItem() { Text = s.Name, Value = s.Code}))
<br />

Fav. Color: @Html.DropDownList("FavColor",


Options.ListColors().Select(c =>
new SelectListItem() { Text = c, Value = c }))
</div>
</body>
</html>

ProfileOptionsService es un servicio de nivel de interfaz de usuario diseñado para proporcionar solo los datos
necesarios para este formulario:

using System.Collections.Generic;

namespace ViewInjectSample.Model.Services
{
public class ProfileOptionsService
{
public List<string> ListGenders()
{
// keeping this simple
return new List<string>() {"Female", "Male"};
}

public List<State> ListStates()


{
// a few states from USA
return new List<State>()
{
new State("Alabama", "AL"),
new State("Alaska", "AK"),
new State("Ohio", "OH")
};
}

public List<string> ListColors()


{
return new List<string>() { "Blue","Green","Red","Yellow" };
}
}
}
TIP
No olvide registrar los tipos que solicitará a través de la inserción de dependencias en el método ConfigureServices en
Startup.cs.

Reemplazar servicios
Además de insertar nuevos servicios, esta técnica también puede usarse para reemplazar servicios previamente
insertados en una página. En la imagen de abajo se muestran todos los campos disponibles en la página usada
en el primer ejemplo:

Como puede ver, los campos predeterminados incluyen Html , Component y Url (además de StatsService que
hemos insertado). Si, por ejemplo, quisiera reemplazar las aplicaciones auxiliares de HTML predeterminadas
con las suyas propias, puede hacerlo fácilmente mediante @inject :

@using System.Threading.Tasks
@using ViewInjectSample.Helpers
@inject MyHtmlHelper Html
<!DOCTYPE html>
<html>
<head>
<title>My Helper</title>
</head>
<body>
<div>
Test: @Html.Value
</div>
</body>
</html>

Si quiere ampliar los servicios existentes, simplemente puede usar esta técnica al heredar o encapsular la
implementación existente con la suya propia.

Vea también
Blog de Simon Timms: Getting Lookup Data Into Your View (Obtener datos de búsqueda en la vista)
Componentes de vista en ASP.NET Core
25/06/2018 • 18 minutes to read • Edit Online

Por Rick Anderson


Vea o descargue el código de ejemplo (cómo descargarlo)

Componentes de vista
Los componentes de vista son similares a las vistas parciales, pero mucho más eficaces. Los componentes de
vista no usan el enlace de modelos y solo dependen de los datos proporcionados cuando se les llama. Este
artículo se escribió en torno a ASP.NET Core MVC, pero los componentes de vista también funcionan con
páginas de Razor.
Un componente de vista:
Representa un fragmento en lugar de una respuesta completa.
Incluye las mismas ventajas de separación de conceptos y capacidad de prueba que se encuentran entre un
controlador y una vista.
Puede tener parámetros y lógica de negocios.
Normalmente se invoca desde una página de diseño.
Los componentes de vista están diseñados para cualquier lugar que tenga lógica de representación reutilizable
demasiado compleja para una vista parcial, como:
Menús de navegación dinámica
Nube de etiquetas (donde consulta la base de datos)
Panel de inicio de sesión
Carro de la compra
Artículos publicados recientemente
Contenido de la barra lateral de un blog típico
Un panel de inicio de sesión que se representa en cada página y muestra los vínculos para iniciar o cerrar
sesión, según el estado del usuario
Un componente de vista consta de dos partes: la clase (normalmente derivada de ViewComponent) y el
resultado que devuelve (por lo general, una vista). Al igual que los controladores, un componente de vista puede
ser un POCO, pero la mayoría de los desarrolladores prefieren aprovechar las ventajas que ofrecen los métodos
y las propiedades disponibles al derivar de ViewComponent .

Crear un componente de vista


Esta sección contiene los requisitos de alto nivel para crear un componente de vista. Más adelante en el artículo
examinaremos cada paso en detalle y crearemos un componente de vista.
La clase de componente de vista
Es posible crear una clase de componente de vista mediante una de las siguientes acciones:
Derivar de ViewComponent
Decorar una clase con el atributo [ViewComponent] o derivar de una clase con el atributo [ViewComponent]
Crear una clase cuyo nombre termina con el sufijo ViewComponent
Al igual que los controladores, los componentes de vista deben ser clases públicas, no anidadas y no abstractas.
El nombre del componente de vista es el nombre de la clase sin el sufijo "ViewComponent". También se puede
especificar explícitamente mediante la propiedad ViewComponentAttribute.Name .
Una clase de componente de vista:
Es totalmente compatible con la inserción de dependencias de constructor.
No participa en el ciclo de vida del controlador, lo que significa que no se pueden usar filtros en un
componente de vista.
Métodos de componente de vista
Un componente de vista define su lógica en un método InvokeAsync que devuelve IViewComponentResult . Los
parámetros proceden directamente de la invocación del componente de vista, no del enlace de modelos. Un
componente de vista nunca controla directamente una solicitud. Por lo general, un componente de vista
inicializa un modelo y lo pasa a una vista mediante una llamada al método View . En resumen, los métodos de
componente de vista:
Definen un método InvokeAsync que devuelve IViewComponentResult .
Por lo general, inicializan un modelo y lo pasan a una vista mediante una llamada al método ViewComponent
View .
Los parámetros provienen del método que realiza la llamada, y no de HTTP, ya que no hay ningún enlace de
modelos.
No son accesibles directamente como un punto de conexión HTTP, sino que se invocan desde el código
(normalmente en una vista). Un componente de vista nunca controla una solicitud.
Se sobrecargan en la firma, en lugar de los detalles de la solicitud HTTP actual.
Ruta de búsqueda de la vista
El tiempo de ejecución busca la vista en las rutas de acceso siguientes:
Views/<nombre_del_contrlador>/Components/<nombre_del_componente_de_vista>/<nombre_de_vista>
Views/Shared/Components/<nombre_del_componente_de_vista>/<nombre_de_vista>
El nombre de vista predeterminado para un componente de vista es Default, lo que significa que el archivo de
vista normalmente se denominará Default.cshtml. Puede especificar un nombre de vista diferente al crear el
resultado del componente de vista o al llamar al método View .
Se recomienda que asigne al archivo de vista el nombre Default.cshtml y que use la ruta de acceso
Views/Shared/Components/<nombre_del_componente_de_vista>/<nombre_de_vista>. El componente de vista
PriorityList usado en este ejemplo usa Views/Shared/Components/PriorityList/Default.cshtml para la vista
del componente de vista.

Invocar un componente de vista


Para usar el componente de vista, llame a lo siguiente dentro de una vista:

@Component.InvokeAsync("Name of view component", <anonymous type containing parameters>)

Los parámetros se pasarán al método InvokeAsync . El componente de vista PriorityList desarrollado en el


artículo se invoca desde el archivo de vista Views/Todo/Index.cshtml. En la tabla siguiente, se llama al método
InvokeAsync con dos parámetros:

@await Component.InvokeAsync("PriorityList", new { maxPriority = 4, isDone = true })


Invocación de un componente de vista como una aplicación auxiliar
de etiquetas
Para ASP.NET Core 1.1 y versiones posteriores, puede invocar un componente de vista como una aplicación
auxiliar de etiquetas:

<vc:priority-list max-priority="2" is-done="false">


</vc:priority-list>

Los parámetros de clase y método con grafía Pascal para las aplicaciones auxiliares de etiquetas se convierten a
su grafía kebab en minúsculas. La aplicación auxiliar de etiquetas que va a invocar un componente de vista usa
el elemento <vc></vc> . El componente de vista se especifica de la manera siguiente:

<vc:[view-component-name]
parameter1="parameter1 value"
parameter2="parameter2 value">
</vc:[view-component-name]>

Nota: Para poder usar un componente de vista como una aplicación auxiliar de etiquetas, debe registrar el
ensamblado que contiene el componente de vista mediante la directiva @addTagHelper . Por ejemplo, si el
componente de vista está en un ensamblado denominado "MyWebApp", agregue la directiva siguiente al
archivo _ViewImports.cshtml :

@addTagHelper *, MyWebApp

Puede registrar un componente de vista como una aplicación auxiliar de etiquetas en cualquier archivo que
haga referencia al componente de vista. Vea Administración del ámbito de las aplicaciones auxiliares de
etiquetas para más información sobre cómo registrar aplicaciones auxiliares de etiquetas.
El método InvokeAsync usado en este tutorial:

@await Component.InvokeAsync("PriorityList", new { maxPriority = 4, isDone = true })

En el marcado de la aplicación auxiliar de etiquetas:

<vc:priority-list max-priority="2" is-done="false">


</vc:priority-list>

En el ejemplo anterior, el componente de vista PriorityList se convierte en priority-list . Los parámetros


para el componente de vista se pasan como atributos en grafía kebab en minúsculas.
Invocar un componente de vista directamente desde un controlador
Los componentes de vista suelen invocarse desde una vista, pero se pueden invocar directamente desde un
método de controlador. Aunque los componentes de vista no definen puntos de conexión como controladores,
se puede implementar fácilmente una acción del controlador que devuelva el contenido de ViewComponentResult
.
En este ejemplo, se llama al componente de vista directamente desde el controlador:
public IActionResult IndexVC()
{
return ViewComponent("PriorityList", new { maxPriority = 3, isDone = false });
}

Tutorial: Creación de un componente de vista simple


Descargue, compile y pruebe el código de inicio. Se trata de un proyecto simple con un controlador Todo que
muestra una lista de tareas pendientes.

Agregar una clase ViewComponent


Cree una carpeta ViewComponents y agregue la siguiente clase PriorityListViewComponent :
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ViewComponentSample.Models;

namespace ViewComponentSample.ViewComponents
{
public class PriorityListViewComponent : ViewComponent
{
private readonly ToDoContext db;

public PriorityListViewComponent(ToDoContext context)


{
db = context;
}

public async Task<IViewComponentResult> InvokeAsync(


int maxPriority, bool isDone)
{
var items = await GetItemsAsync(maxPriority, isDone);
return View(items);
}
private Task<List<TodoItem>> GetItemsAsync(int maxPriority, bool isDone)
{
return db.ToDo.Where(x => x.IsDone == isDone &&
x.Priority <= maxPriority).ToListAsync();
}
}
}

Notas sobre el código:


Las clases de componentes de vista pueden estar contenidas en cualquier carpeta del proyecto.
Dado que el nombre de clase PriorityListViewComponent acaba con el sufijo ViewComponent, el
tiempo de ejecución usará la cadena "PriorityList" cuando haga referencia al componente de clase desde
una vista. Más adelante explicaremos esta cuestión con más detalle.
El atributo [ViewComponent] puede cambiar el nombre usado para hacer referencia a un componente de
vista. Por ejemplo, podríamos haber asignado a la clase el nombre XYZ y aplicado el atributo
ViewComponent :

[ViewComponent(Name = "PriorityList")]
public class XYZ : ViewComponent

El atributo [ViewComponent]anterior le indica al selector de componentes de vista que use el nombre


PriorityList al buscar las vistas asociadas al componente y que use la cadena "PriorityList" al hacer
referencia al componente de clase desde una vista. Más adelante explicaremos esta cuestión con más
detalle.
El componente usa la inserción de dependencias para que el contexto de datos esté disponible.
InvokeAsync expone un método al que se puede llamar desde una vista y puede tomar un número
arbitrario de argumentos.
El método InvokeAsync devuelve el conjunto de elementos ToDo que cumplen los parámetros isDone y
maxPriority .
Crear la vista de Razor del componente de vista
Cree la carpeta Views/Shared/Components. Esta carpeta debe denominarse Components.
Cree la carpeta Views/Shared/Components/PriorityList. El nombre de esta carpeta debe coincidir con el
nombre de la clase de componente de vista o con el nombre de la clase sin el sufijo (si se ha seguido la
convención y se ha usado el sufijo ViewComponent en el nombre de clase). Si ha usado el atributo
ViewComponent , el nombre de clase debe coincidir con la designación del atributo.

Cree una vista de Razor Views/Shared/Components/PriorityList/Default.cshtml: [!code-cshtml]


La vista de Razor toma una lista de TodoItem y muestra estos elementos. Si el método InvokeAsync del
componente de vista no pasa el nombre de la vista (como en nuestro ejemplo), se usa Default para el
nombre de vista, según la convención. Más adelante en el tutorial veremos cómo pasar el nombre de la
vista. Para reemplazar el estilo predeterminado de un controlador concreto, agregue una vista a la
carpeta de vistas específicas del controlador (por ejemplo,
Views/Todo/Components/PriorityList/Default.cshtml).
Si el componente de vista es específico del controlador, puede agregarlo a la carpeta específica del
controlador (Views/Todo/Components/PriorityList/Default.cshtml).
Agregue un elemento div que contenga una llamada al componente de lista de prioridad en la parte
inferior del archivo Views/Todo/index.cshtml:

</table>
<div>
@await Component.InvokeAsync("PriorityList", new { maxPriority = 2, isDone = false })
</div>

El marcado @await Component.InvokeAsync muestra la sintaxis para llamar a los componentes de vista. El primer
argumento es el nombre del componente que se quiere invocar o llamar. Los parámetros siguientes se pasan al
componente. InvokeAsync puede tomar un número arbitrario de argumentos.
Pruebe la aplicación. En la imagen siguiente se muestra la lista de tareas pendientes y los elementos de
prioridad:
También puede llamar al componente de vista directamente desde el controlador:

public IActionResult IndexVC()


{
return ViewComponent("PriorityList", new { maxPriority = 3, isDone = false });
}

Especificar un nombre de vista


En algunos casos, puede ser necesario que un componente de vista complejo especifique una vista no
predeterminada. En el código siguiente se muestra cómo especificar la vista "PVC" desde el método
InvokeAsync . Actualice el método InvokeAsync en la clase PriorityListViewComponent .

public async Task<IViewComponentResult> InvokeAsync(


int maxPriority, bool isDone)
{
string MyView = "Default";
// If asking for all completed tasks, render with the "PVC" view.
if (maxPriority > 3 && isDone == true)
{
MyView = "PVC";
}
var items = await GetItemsAsync(maxPriority, isDone);
return View(MyView, items);
}

Copie el archivo Views/Shared/Components/PriorityList/Default.cshtml en una vista denominada


Views/Shared/Components/PriorityList/PVC.cshtml. Agregue un encabezado para indicar que se está usando la
vista PVC.

@model IEnumerable<ViewComponentSample.Models.TodoItem>

<h2> PVC Named Priority Component View</h2>


<h4>@ViewBag.PriorityMessage</h4>
<ul>
@foreach (var todo in Model)
{
<li>@todo.Name</li>
}
</ul>

Actualice Views/TodoList/Index.cshtml:
@await Component.InvokeAsync("PriorityList", new { maxPriority = 4, isDone = true })

Ejecute la aplicación y compruebe la vista PVC.

Si la vista PVC no se representa, compruebe que está llamando al componente de vista con prioridad cuatro o
superior.
Examinar la ruta de acceso de la vista
Cambie el parámetro de prioridad a tres o menos para que no se devuelva la vista de prioridad.
Cambie temporalmente el nombre de Views/Todo/Components/PriorityList/Default.cshtml a
1Default.cshtml.
Pruebe la aplicación. Obtendrá el siguiente error:

An unhandled exception occurred while processing the request.


InvalidOperationException: The view 'Components/PriorityList/Default' wasn't found. The following
locations were searched:
/Views/ToDo/Components/PriorityList/Default.cshtml
/Views/Shared/Components/PriorityList/Default.cshtml
EnsureSuccessful

Copie Views/Todo/Components/PriorityList/1Default.cshtml en
Views/Shared/Components/PriorityList/Default.cshtml.
Agregue algún marcado a la vista de componentes de vista de la lista de tareas pendientes Shared para
indicar que la vista está en la carpeta Shared.
Pruebe la vista de componentes Shared.
Evitar cadenas mágicas
Si busca seguridad en tiempo de compilación, puede reemplazar el nombre del componente de vista codificado
de forma rígida por el nombre de clase. Cree el componente de vista sin el sufijo "ViewComponent":

using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ViewComponentSample.Models;

namespace ViewComponentSample.ViewComponents
{
public class PriorityList : ViewComponent
{
private readonly ToDoContext db;

public PriorityList(ToDoContext context)


{
db = context;
}

public async Task<IViewComponentResult> InvokeAsync(


int maxPriority, bool isDone)
{
var items = await GetItemsAsync(maxPriority, isDone);
return View(items);
}
private Task<List<TodoItem>> GetItemsAsync(int maxPriority, bool isDone)
{
return db.ToDo.Where(x => x.IsDone == isDone &&
x.Priority <= maxPriority).ToListAsync();
}
}
}

Agregue una instrucción using para su archivo de vista de Razor y use el operador nameof :
@using ViewComponentSample.Models
@using ViewComponentSample.ViewComponents
@model IEnumerable<TodoItem>

<h2>ToDo nameof</h2>
<!-- Markup removed for brevity. -->

<div>

@await Component.InvokeAsync(nameof(PriorityList), new { maxPriority = 4, isDone = true })


</div>

Recursos adicionales
Inserción de dependencias en vistas
Control de solicitudes con controladores en ASP.NET
Core MVC
25/06/2018 • 11 minutes to read • Edit Online

Por Steve Smith y Scott Addie


Los controladores, las acciones y los resultados de acciones son una parte fundamental del proceso que siguen los
desarrolladores para compilar aplicaciones con ASP.NET Core MVC.

¿Qué es un controlador?
Los controladores se usan para definir y agrupar un conjunto de acciones. Una acción (o método de acción) es un
método en un controlador que controla las solicitudes. Los controladores agrupan lógicamente acciones similares.
Esta agregación de acciones permite aplicar de forma colectiva conjuntos comunes de reglas, como el
enrutamiento, el almacenamiento en caché y la autorización. Las solicitudes se asignan a acciones mediante el
enrutamiento.
Por convención, las clases de controlador:
Residen en la carpeta Controllers del nivel de raíz del proyecto.
Heredan de Microsoft.AspNetCore.Mvc.Controller .
Un controlador es una clase instanciable en la que se cumple al menos una de las siguientes condiciones:
El nombre de clase tiene el sufijo "Controller".
La clase hereda de una clase cuyo nombre tiene el sufijo "Controller".
La clase está decorada con el atributo [Controller] .
Una clase de controlador no debe tener asociado un atributo [NonController] .
Los controladores deben seguir el principio de dependencias explícitas. Existen dos maneras de implementar este
principio. Si varias acciones de controlador requieren el mismo servicio, considere la posibilidad de usar la
inserción de constructores para solicitar esas dependencias. Si el servicio solo lo requiere un único método de
acción, considere la posibilidad de usar la inserción de acciones para solicitar la dependencia.
En el patrón de Model-View -Controller, un controlador se encarga del procesamiento inicial de la solicitud y la
creación de instancias del modelo. Por lo general, las decisiones empresariales deben realizarse dentro del
modelo.
El controlador toma el resultado del procesamiento del modelo (si existe) y devuelve o bien la vista correcta y sus
datos de vista asociados, o bien el resultado de la llamada API. Obtenga más información en Información general
de ASP.NET Core MVC e Introducción a ASP.NET Core MVC y Visual Studio.
El controlador es una abstracción de nivel de interfaz de usuario. Se encarga de garantizar que los datos de la
solicitud son válidos y de elegir qué vista (o resultado de una API) se debe devolver. En las aplicaciones
factorizadas correctamente, no incluye directamente acceso a datos ni lógica de negocios. En su lugar, el
controlador delega en servicios el control de estas responsabilidades.

Definir acciones
Los métodos públicos de un controlador, excepto los que están decorados con el atributo [NonAction] , son
acciones. Los parámetros de las acciones están enlazados a los datos de la solicitud y se validan mediante el
enlace de modelos. La validación de modelos se lleva a cabo para todo lo que está enlazado a un modelo. El valor
de la propiedad ModelState.IsValid indica si el enlace de modelos y la validación se han realizado correctamente.
Los métodos de acción deben contener lógica para asignar una solicitud a una cuestión empresarial.
Normalmente las cuestiones empresariales se deben representar como servicios a los que el controlador obtiene
acceso mediante la inserción de dependencias. Después, las acciones asignan el resultado de la acción empresarial
a un estado de aplicación.
Las acciones pueden devolver de todo, pero suelen devolver una instancia de IActionResult (o
Task<IActionResult> para métodos asincrónicos) que genera una respuesta. El método de acción se encarga de
elegir el tipo de respuesta. El resultado de la acción se encarga de la respuesta.
Métodos auxiliares de controlador
Los controladores normalmente heredan de Controller, aunque esto no es necesario. Al derivar de Controller , se
proporciona acceso a tres categorías de métodos auxiliares:
1. Métodos que producen un cuerpo de respuesta vacío
No se incluye ningún encabezado de respuesta HTTP Content-Type , ya que el cuerpo de la respuesta no tiene
contenido que describir.
Hay dos tipos de resultados en esta categoría: redireccionamiento y código de estado HTTP.
Código de estado HTTP
Este tipo devuelve un código de estado HTTP. BadRequest , NotFound y Ok son ejemplos de métodos
auxiliares de este tipo. Por ejemplo, return BadRequest(); genera un código de estado 400 cuando se
ejecuta. Cuando métodos como BadRequest , NotFound y Ok están sobrecargados, ya no se consideran
respondedores de código de estado HTTP, dado que se lleva a cabo una negociación de contenido.
Redireccionamiento
Este tipo devuelve un redireccionamiento a una acción o destino (mediante Redirect , LocalRedirect ,
RedirectToAction o RedirectToRoute ). Por ejemplo, return RedirectToAction("Complete", new {id = 123});
pasa un objeto anónimo y redirige a Complete .
El tipo de resultado de redireccionamiento difiere del tipo de código de estado HTTP principalmente en que
se agrega un encabezado de respuesta HTTP Location .
2. Métodos que producen un cuerpo de respuesta no vacío con un tipo de contenido predefinido
La mayoría de los métodos auxiliares de esta categoría incluye una propiedad ContentType , lo que permite
establecer el encabezado de respuesta Content-Type para describir el cuerpo de la respuesta.
Hay dos tipos de resultados en esta categoría: vista y respuesta con formato.
Vista
Este tipo devuelve una vista que usa un modelo para representar HTML. Por ejemplo,
return View(customer); pasa un modelo a la vista para el enlace de datos.

Respuesta con formato


Este tipo devuelve JSON o un formato similar de intercambio de datos para representar un objeto de una
manera específica. Por ejemplo, return Json(customer); serializa el objeto proporcionado en formato
JSON.
Otros métodos comunes de este tipo son File , y VirtualFile . Por ejemplo,
PhysicalFile
return PhysicalFile(customerFilePath, "text/xml"); devuelve un archivo XML descrito por un valor de
encabezado de respuesta Content-Type de "text/xml".
3. Métodos que producen un cuerpo de respuesta no vacío con formato en un tipo de contenido negociado con el cliente
Esta categoría también se conoce como negociación de contenido. La negociación de contenido se aplica
siempre que una acción devuelve un tipo ObjectResult o un valor distinto de una implementación IActionResult.
Si una acción devuelve una implementación que no sea IActionResult (por ejemplo, object ), también devolverá
una respuesta con formato.
Algunos métodos auxiliares de este tipo son BadRequest , CreatedAtRoute y Ok . Algunos ejemplos de estos
métodos son return BadRequest(modelState); , return CreatedAtRoute("routename", values, newobject); y
return Ok(value); , respectivamente. Tenga en cuenta que BadRequest y Ok realizan la negociación de contenido
solo cuando se pasa un valor; si no se pasa un valor, actúan como tipos de resultado de código de estado HTTP.
Por otro lado, el método CreatedAtRoute siempre realiza la negociación de contenido, ya que todas sus
sobrecargas requieren que se pase un valor.
Cuestiones transversales
Normalmente, las aplicaciones comparten partes de su flujo de trabajo. Un ejemplo de esto podría ser una
aplicación que requiere autenticación para obtener acceso al carro de la compra o una aplicación que almacena en
caché datos en algunas páginas. Para llevar a cabo la lógica antes o después de un método de acción, use un filtro.
El uso de filtros en cuestiones transversales puede reducir la duplicación, lo que permite seguir el principio "Una
vez y solo una" (DRY ).
La mayoría de los atributos de filtro, como [Authorize] , se puede aplicar en el nivel de controlador o de acción en
función del nivel deseado de granularidad.
El control de errores y el almacenamiento en caché de respuestas suelen ser cuestiones transversales:
Control de errores
Almacenamiento en caché de respuestas
Es posible controlar muchas cuestiones transversales mediante el uso de filtros o software intermedio
personalizado.
Enrutar a acciones de controlador de ASP.NET
Core
25/06/2018 • 59 minutes to read • Edit Online

Por Ryan Nowak y Rick Anderson


ASP.NET Core MVC utiliza el middleware de enrutamiento para buscar las direcciones URL de las
solicitudes entrantes y asignarlas a acciones. Las rutas se definen en el código de inicio o los atributos.
Las rutas describen cómo se deben asociar las rutas de dirección URL a las acciones. Las rutas también
se usan para generar direcciones URL (para vínculos) enviadas en las respuestas.
Las acciones se enrutan bien mediante convención o bien mediante atributos. Colocar una ruta en el
controlador o la acción hace que se enrute mediante atributos. Consulte Enrutamiento mixto para
obtener más información.
Este documento explica las interacciones entre MVC y el enrutamiento, así como el uso que las
aplicaciones MVC suelen hacer de las características de enrutamiento. Consulte Enrutamiento para
obtener más información sobre enrutamiento avanzado.

Configurar el middleware de enrutamiento


En su método Configure puede ver código similar al siguiente:

app.UseMvc(routes =>
{
routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
});

Dentro de la llamada a UseMvc , MapRoute se utiliza para crear una ruta única, a la que nos referiremos
como la ruta default . La mayoría de las aplicaciones MVC usarán una ruta con una plantilla similar a la
ruta default .
La plantilla de ruta "{controller=Home}/{action=Index}/{id?}" puede coincidir con una ruta de dirección
URL como /Products/Details/5 y extraerá los valores de ruta
{ controller = Products, action = Details, id = 5 } mediante la conversión de la ruta en tokens. MVC
intentará encontrar un controlador denominado ProductsController y ejecutar la acción Details :

public class ProductsController : Controller


{
public IActionResult Details(int id) { ... }
}

Tenga en cuenta que, en este ejemplo, el enlace de modelos usaría el valor de id = 5 para establecer el
parámetro id en 5 al invocar esta acción. Consulte Enlace de modelos para obtener más detalles.
Utilizando la ruta default :

routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");

La plantilla de ruta:
{controller=Home} define Home como el valor controller predeterminado
{action=Index} define Index como el valor action predeterminado
{id?} define id como opcional
No es necesario que los parámetros de ruta opcionales y predeterminados estén presentes en la ruta de
dirección URL para una coincidencia. Consulte Referencia de plantilla de ruta para obtener una
descripción detallada de la sintaxis de la plantilla de ruta.
"{controller=Home}/{action=Index}/{id?}" puede coincidir con la ruta de dirección URL / y generará
los valores de ruta { controller = Home, action = Index } . Los valores de controller y action utilizan
los valores predeterminados, id no genera un valor porque no hay ningún segmento correspondiente
en la ruta de dirección URL. MVC utilizaría estos valores de ruta para seleccionar HomeController y la
acción Index :

public class HomeController : Controller


{
public IActionResult Index() { ... }
}

Usando esta definición de controlador y la plantilla de ruta, la acción HomeController.Index se ejecutaría


para cualquiera de las rutas de dirección URL siguientes:
/Home/Index/17

/Home/Index

/Home

El método de conveniencia UseMvcWithDefaultRoute :

app.UseMvcWithDefaultRoute();

Se puede usar para reemplazar:

app.UseMvc(routes =>
{
routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
});

UseMvc y UseMvcWithDefaultRoute agregan una instancia de RouterMiddleware a la canalización de


middleware. MVC no interactúa directamente con middleware y usa el enrutamiento para controlar las
solicitudes. MVC está conectado a las rutas a través de una instancia de MvcRouteHandler . El código
dentro de UseMvc es similar al siguiente:
var routes = new RouteBuilder(app);

// Add connection to MVC, will be hooked up by calls to MapRoute.


routes.DefaultHandler = new MvcRouteHandler(...);

// Execute callback to register routes.


// routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");

// Create route collection and add the middleware.


app.UseRouter(routes.Build());

UseMvc no define directamente ninguna ruta, sino que agrega un marcador de posición a la colección de
rutas para la ruta attribute . La sobrecarga UseMvc(Action<IRouteBuilder>) le permite agregar sus
propias rutas y también admite el enrutamiento mediante atributos. UseMvc y todas sus variaciones
agregan un marcador de posición para la ruta de atributo (el enrutamiento mediante atributos siempre
está disponible, independientemente de cómo configure UseMvc . UseMvcWithDefaultRoute define una
ruta predeterminada y admite el enrutamiento mediante atributos. La sección Enrutamiento mediante
atributos incluye más detalles sobre este tipo de enrutamiento.

Enrutamiento convencional
La ruta default :

routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");

es un ejemplo de un enrutamiento convencional. Llamamos a este estilo enrutamiento convencional


porque establece una convención para las rutas de dirección URL:
El primer segmento de la ruta asigna el nombre de controlador.
El segundo asigna el nombre de la acción.
El tercer segmento se utiliza para un identificador id opcional usado para asignar un modelo de
entidad
Mediante esta ruta , la ruta de dirección URL /Products/List se asigna a la acción
default
ProductsController.List , y /Blog/Article/17 se asigna a BlogController.Article . Esta asignación solo
se basa en los nombres de acción y controlador, y no en espacios de nombres, ubicaciones de archivos de
origen ni parámetros de método.

TIP
Usar el enrutamiento convencional con la ruta predeterminada permite generar rápidamente la aplicación sin
necesidad de elaborar un nuevo patrón de dirección URL para cada acción que se define. Para una aplicación con
acciones de estilo CRUD, la coherencia en las direcciones URL en todos los controladores puede ayudar a
simplificar el código y hacer que la interfaz de usuario sea más predecible.
WARNING
id se define como opcional mediante la plantilla de ruta, lo que significa que las acciones se pueden ejecutar sin
el identificador proporcionado como parte de la dirección URL. Normalmente lo que ocurrirá si id se omite de la
dirección URL es que el enlace de modelos establecerá en 0 su valor y, como resultado, no se encontrará
ninguna entidad en la base de datos que coincida con id == 0 . El enrutamiento mediante atributos proporciona
un mayor control que permite requerir el identificador para algunas acciones y para otras no. Por convención, la
documentación incluirá parámetros opcionales como id cuando sea más probable que su uso sea correcto.

Varias rutas
Para agregar varias rutas dentro de UseMvc , agregue más llamadas a MapRoute . Hacerlo le permite
definir varias convenciones o agregar rutas convencionales que se dedican a una acción específica, como
en el ejemplo siguiente:

app.UseMvc(routes =>
{
routes.MapRoute("blog", "blog/{*article}",
defaults: new { controller = "Blog", action = "Article" });
routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
});

Aquí, la ruta blog es una ruta convencional dedicada, lo que significa que utiliza el sistema de
enrutamiento convencional, pero que está dedicada a una acción específica. Puesto que controller y
action no aparecen en la plantilla de ruta como parámetros, solo pueden tener los valores
predeterminados y, por tanto, esta ruta siempre se asignará a la acción BlogController.Article .
Las rutas de la colección de rutas están ordenadas y se procesan en el orden en que se hayan agregado.
Por tanto, la ruta blog de este ejemplo se intentará antes que la ruta default .

NOTE
Las rutas convencionales dedicadas suelen usar parámetros de ruta comodín como {*article} para capturar
la parte restante de la ruta de dirección URL. Esto puede hacer que la ruta sea "demasiado expansiva", lo que
significa que coincidirá con direcciones URL que se pretendía que coincidieran con otras rutas. Coloque las rutas
"expansivas" más adelante en la tabla de rutas para resolver este problema.

Reserva
Como parte del procesamiento de la solicitud, MVC comprobará que los valores de ruta se pueden usar
para buscar un controlador y la acción en la aplicación. Si los valores de ruta no coinciden con una
acción, entonces la ruta no se considera una coincidencia y se intentará la ruta siguiente. Esto se
denomina reserva, y se ha diseñado para simplificar los casos donde se superponen rutas
convencionales.
Eliminar la ambigüedad de acciones
Cuando dos acciones coinciden en el enrutamiento, MVC debe eliminar la ambigüedad para elegir el
candidato "recomendado", o de lo contrario se produce una excepción. Por ejemplo:
public class ProductsController : Controller
{
public IActionResult Edit(int id) { ... }

[HttpPost]
public IActionResult Edit(int id, Product product) { ... }
}

Este controlador define dos acciones que coincidirían con la ruta de dirección URL /Products/Edit/17 y
los datos de ruta { controller = Products, action = Edit, id = 17 } . Se trata de un patrón típico para
controladores MVC donde Edit(int) muestra un formulario para editar un producto, y
Edit(int, Product) procesa el formulario expuesto. Para hacer esto posible, MVC necesita seleccionar
Edit(int, Product) cuando la solicitud es HTTP POST y Edit(int) cuando el verbo HTTP es cualquier
otra cosa.
HttpPostAttribute ( [HttpPost] ) es una implementación de IActionConstraint que solo permitirá que
la acción se seleccione cuando el verbo HTTP sea POST . La presencia de IActionConstraint hace que
Edit(int, Product) sea una mejor coincidencia que Edit(int) , por lo que Edit(int, Product) se
intentará en primer lugar.
Solo necesitará escribir implementaciones de IActionConstraint personalizadas en escenarios
especializados, pero es importante comprender el rol de atributos como HttpPostAttribute (para otros
verbos HTTP se definen atributos similares). En el enrutamiento convencional es común que las acciones
utilicen el mismo nombre de acción cuando son parte de un flujo de trabajo show form -> submit form .
La comodidad de este patrón será más evidente después de revisar la sección Descripción de
IActionConstraint.
Si coinciden varias rutas y MVC no encuentra una ruta "recomendada", se produce una excepción
AmbiguousActionException .

Nombres de ruta
Las cadenas "blog" y "default" de los ejemplos siguientes son nombres de ruta:

app.UseMvc(routes =>
{
routes.MapRoute("blog", "blog/{*article}",
defaults: new { controller = "Blog", action = "Article" });
routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
});

Los nombres de ruta proporcionan un nombre lógico a la ruta para que la ruta con nombre pueda
utilizarse en la generación de direcciones URL. Esto simplifica en gran medida la creación de direcciones
URL en los casos en que el orden de las rutas podría complicar la generación de direcciones URL. Los
nombres de ruta deben ser únicos en toda la aplicación.
Los nombres de ruta no tienen ningún impacto en la coincidencia de direcciones URL ni en el control de
las solicitudes; se utilizan únicamente para la generación de direcciones URL. En Enrutamiento se incluye
información más detallada sobre la generación de direcciones URL, como la generación de direcciones
URL en aplicaciones auxiliares específicas de MVC.

Enrutamiento mediante atributos


El enrutamiento mediante atributos utiliza un conjunto de atributos para asignar acciones directamente a
las plantillas de ruta. En el ejemplo siguiente, app.UseMvc(); se utiliza en el método Configure y no se
pasa ninguna ruta. HomeController coincidirá con un conjunto de direcciones URL similares a las que
coincidirían con la ruta predeterminada {controller=Home}/{action=Index}/{id?} :

public class HomeController : Controller


{
[Route("")]
[Route("Home")]
[Route("Home/Index")]
public IActionResult Index()
{
return View();
}
[Route("Home/About")]
public IActionResult About()
{
return View();
}
[Route("Home/Contact")]
public IActionResult Contact()
{
return View();
}
}

La acción HomeController.Index() se ejecutará para cualquiera de las rutas de dirección URL / , /Home
o /Home/Index .

NOTE
Este ejemplo resalta una diferencia clave de programación entre el enrutamiento mediante atributos y el
enrutamiento convencional. El enrutamiento mediante atributos requiere más entradas para especificar una ruta;
la ruta predeterminada convencional controla las rutas de manera más sucinta. Pero el enrutamiento mediante
atributos permite (y requiere) un control preciso de las plantillas de ruta que se aplicarán a cada acción.

Con el enrutamiento mediante atributos, el nombre del controlador y los nombres de acción no juegan
ningún papel en la selección de las acciones. Este ejemplo coincidirá con las mismas direcciones URL
que el ejemplo anterior.

public class MyDemoController : Controller


{
[Route("")]
[Route("Home")]
[Route("Home/Index")]
public IActionResult MyIndex()
{
return View("Index");
}
[Route("Home/About")]
public IActionResult MyAbout()
{
return View("About");
}
[Route("Home/Contact")]
public IActionResult MyContact()
{
return View("Contact");
}
}
NOTE
Las plantillas de ruta anteriores no definen parámetros de ruta para action , area y controller . De hecho,
estos parámetros de ruta no se permiten en rutas de atributo. Puesto que la plantilla de ruta ya está asociada a
una acción, no tendría sentido analizar el nombre de acción de la dirección URL.

Enrutamiento mediante atributos con atributos Http[Verb]


El enrutamiento mediante atributos también puede hacer uso de los atributos Http[Verb] como
HttpPostAttribute . Todos estos atributos pueden aceptar una plantilla de ruta. Este ejemplo muestra dos
acciones que coinciden con la misma plantilla de ruta:

[HttpGet("/products")]
public IActionResult ListProducts()
{
// ...
}

[HttpPost("/products")]
public IActionResult CreateProduct(...)
{
// ...
}

Para una ruta de dirección URL como /products , la acción ProductsApi.ListProducts se ejecutará
cuando el verbo HTTP sea GET y ProductsApi.CreateProduct se ejecutará cuando el verbo HTTP sea
POST . El enrutamiento mediante atributos primero busca una coincidencia de la dirección URL en el
conjunto de plantillas de ruta que se definen mediante atributos de ruta. Una vez que se encuentra una
plantilla de ruta que coincide, las restricciones IActionConstraint se aplican para determinar qué
acciones se pueden ejecutar.

TIP
Cuando se compila una API de REST, es poco frecuente que se quiera usar [Route(...)] en un método de
acción. Es mejor usar Http*Verb*Attributes más específicos para precisar lo que es compatible con la API. Se
espera que los clientes de API de REST sepan qué rutas y verbos HTTP se asignan a determinadas operaciones
lógicas.

Puesto que una ruta de atributo se aplica a una acción específica, es fácil crear parámetros necesarios
como parte de la definición de plantilla de ruta. En este ejemplo, se requiere id como parte de la ruta
de dirección URL.

public class ProductsApiController : Controller


{
[HttpGet("/products/{id}", Name = "Products_List")]
public IActionResult GetProduct(int id) { ... }
}

La acción ProductsApi.GetProduct(int) se ejecutará para una ruta de dirección URL como /products/3 ,
pero no para una ruta de dirección URL como /products . Consulte Enrutamiento para obtener una
descripción completa de las plantillas de ruta y las opciones relacionadas.

Nombre de ruta
El código siguiente define un nombre de ruta de Products_List :

public class ProductsApiController : Controller


{
[HttpGet("/products/{id}", Name = "Products_List")]
public IActionResult GetProduct(int id) { ... }
}

Los nombres de ruta se pueden utilizar para generar una dirección URL basada en una ruta específica.
Los nombres de ruta no tienen ningún impacto en el comportamiento de coincidencia de direcciones
URL del enrutamiento, y solo se usan para la generación de direcciones URL. Los nombres de ruta
deben ser únicos en toda la aplicación.

NOTE
Compare esto con la ruta predeterminada convencional, que define el parámetro id como opcional ( {id?} ).
Esta capacidad de especificar con precisión las API tiene sus ventajas, como permitir que /products y
/products/5 se envíen a acciones diferentes.

Combinación de rutas
Para que el enrutamiento mediante atributos sea menos repetitivo, los atributos de ruta del controlador
se combinan con los atributos de ruta de las acciones individuales. Las plantillas de ruta definidas en el
controlador se anteponen a las plantillas de ruta de las acciones. La colocación de un atributo de ruta en
el controlador hace que todas las acciones del controlador usen el enrutamiento mediante atributos.

[Route("products")]
public class ProductsApiController : Controller
{
[HttpGet]
public IActionResult ListProducts() { ... }

[HttpGet("{id}")]
public ActionResult GetProduct(int id) { ... }
}

En este ejemplo, la ruta de dirección URL /products puede coincidir con ProductsApi.ListProducts y la
ruta de dirección URL /products/5 puede coincidir con ProductsApi.GetProduct(int) . Ambas acciones
coinciden solo con HTTP GET porque incluyen HttpGetAttribute .
Las plantillas de ruta aplicadas a una acción que comienzan por / no se combinan con las plantillas de
ruta que se aplican al controlador. En este ejemplo coinciden un conjunto de rutas de dirección URL
similares a la ruta predeterminada.
[Route("Home")]
public class HomeController : Controller
{
[Route("")] // Combines to define the route template "Home"
[Route("Index")] // Combines to define the route template "Home/Index"
[Route("/")] // Doesn't combine, defines the route template ""
public IActionResult Index()
{
ViewData["Message"] = "Home index";
var url = Url.Action("Index", "Home");
ViewData["Message"] = "Home index" + "var url = Url.Action; = " + url;
return View();
}

[Route("About")] // Combines to define the route template "Home/About"


public IActionResult About()
{
return View();
}
}

Ordenación de rutas de atributo


A diferencia de las rutas convencionales que se ejecutan en un orden definido, el enrutamiento mediante
atributos genera un árbol y busca coincidir con todas las rutas al mismo tiempo. Es como si las entradas
de ruta se colocasen en un orden ideal; las rutas más específicas tienen la oportunidad de ejecutarse
antes que las rutas más generales.
Por ejemplo, una ruta como blog/search/{topic} es más específica que una ruta como blog/{*article} .
Desde un punto de vista lógico, la ruta blog/search/{topic} se ejecuta en primer lugar de forma
predeterminada, ya que ese es el único orden significativo. En el enrutamiento convencional, el
desarrollador es responsable de colocar las rutas en el orden deseado.
Las rutas de atributo pueden configurar un orden mediante la propiedad Order de todos los atributos
de ruta proporcionados por el marco. Las rutas se procesan de acuerdo con el orden ascendente de la
propiedad Order . El orden predeterminado es 0 . Si una ruta se configura con Order = -1 , se ejecutará
antes que las rutas que establecen un orden. Si una ruta se configura con Order = 1 , se ejecutará
después del orden de rutas predeterminado.

TIP
Evite depender de Order . Si su espacio de direcciones URL requiere unos valores de orden explícitos para un
enrutamiento correcto, es probable que también sea confuso para los clientes. Por lo general, el enrutamiento
mediante atributos seleccionará la ruta correcta con la coincidencia de dirección URL. Si el orden predeterminado
que se usa para la generación de direcciones URL no funciona, normalmente es más sencillo utilizar el nombre de
ruta como una invalidación que aplicar la propiedad Order .

Reemplazo de tokens en plantillas de ruta ([controller], [action],


[area])
Para mayor comodidad, las rutas de atributo admiten reemplazo de token. Para ello, incluyen un token
entre corchetes ( [ , ] ). Los tokens [action] , [area] y [controller] se reemplazarán por los valores
del nombre de la acción, el nombre del área y el nombre del controlador de la acción donde se define la
ruta. En este ejemplo, las acciones pueden coincidir con las rutas de dirección URL, tal como se describe
en los comentarios:
[Route("[controller]/[action]")]
public class ProductsController : Controller
{
[HttpGet] // Matches '/Products/List'
public IActionResult List() {
// ...
}

[HttpGet("{id}")] // Matches '/Products/Edit/{id}'


public IActionResult Edit(int id) {
// ...
}
}

El reemplazo del token se produce como último paso de la creación de las rutas de atributo. El ejemplo
anterior se comportará igual que el código siguiente:

public class ProductsController : Controller


{
[HttpGet("[controller]/[action]")] // Matches '/Products/List'
public IActionResult List() {
// ...
}

[HttpGet("[controller]/[action]/{id}")] // Matches '/Products/Edit/{id}'


public IActionResult Edit(int id) {
// ...
}
}

Las rutas de atributo también se pueden combinar con la herencia. Esto es especialmente eficaz si se
combina con el reemplazo de token.

[Route("api/[controller]")]
public abstract class MyBaseController : Controller { ... }

public class ProductsController : MyBaseController


{
[HttpGet] // Matches '/api/Products'
public IActionResult List() { ... }

[HttpPut("{id}")] // Matches '/api/Products/{id}'


public IActionResult Edit(int id) { ... }
}

El reemplazo de token también se aplica a los nombres de ruta definidos por las rutas de atributo.
[Route("[controller]/[action]", Name="[controller]_[action]")] generará un nombre de ruta único para
cada acción.
Para que el delimitador de reemplazo de token [ o ] coincida, repita el carácter ( [[ o ]] ) para
usarlo como secuencia de escape.
Varias rutas
El enrutamiento mediante atributos permite definir varias rutas que llegan a la misma acción. El uso más
común de esto es imitar el comportamiento de la ruta convencional predeterminada tal como se
muestra en el ejemplo siguiente:
[Route("[controller]")]
public class ProductsController : Controller
{
[Route("")] // Matches 'Products'
[Route("Index")] // Matches 'Products/Index'
public IActionResult Index()
}

La colocación de varios atributos de ruta en el controlador significa que cada uno de ellos se combinará
con cada uno de los atributos de ruta en los métodos de acción.

[Route("Store")]
[Route("[controller]")]
public class ProductsController : Controller
{
[HttpPost("Buy")] // Matches 'Products/Buy' and 'Store/Buy'
[HttpPost("Checkout")] // Matches 'Products/Checkout' and 'Store/Checkout'
public IActionResult Buy()
}

Cuando en una acción se colocan varios atributos de ruta (que implementan IActionConstraint ), cada
restricción de acción se combina con la plantilla de ruta del atributo que lo define.

[Route("api/[controller]")]
public class ProductsController : Controller
{
[HttpPut("Buy")] // Matches PUT 'api/Products/Buy'
[HttpPost("Checkout")] // Matches POST 'api/Products/Checkout'
public IActionResult Buy()
}

TIP
Si bien el uso de varias rutas en acciones puede parecer eficaz, es mejor que el espacio de direcciones URL de la
aplicación sea sencillo y esté bien definido. Utilice varias rutas en acciones solo cuando sea necesario, por ejemplo,
para admitir a clientes existentes.

Especificación de parámetros opcionales de ruta de atributo, valores predeterminados y restricciones


Las rutas de atributo admiten la misma sintaxis en línea que las rutas convencionales para especificar
parámetros opcionales, valores predeterminados y restricciones.

[HttpPost("product/{id:int}")]
public IActionResult ShowProduct(int id)
{
// ...
}

Consulte Referencia de plantilla de ruta para obtener una descripción detallada de la sintaxis de la
plantilla de ruta.
Atributos de ruta personalizados con IRouteTemplateProvider

Todos los atributos de ruta proporcionados en el marco ( [Route(...)] , [HttpGet(...)] , etc.)


implementan la interfaz IRouteTemplateProvider . Al iniciarse la aplicación, MVC busca atributos en las
clases de controlador y los métodos de acción, y usa los que implementan IRouteTemplateProvider para
generar el conjunto inicial de rutas.
Implemente IRouteTemplateProvider para definir sus propios atributos de ruta. Cada
IRouteTemplateProvider le permite definir una única ruta con una plantilla de ruta, un orden y un nombre
personalizados:

public class MyApiControllerAttribute : Attribute, IRouteTemplateProvider


{
public string Template => "api/[controller]";

public int? Order { get; set; }

public string Name { get; set; }


}

El atributo del ejemplo anterior establece automáticamente Template en "api/[controller]" cuando se


aplica [MyApiController] .
Uso del modelo de aplicación para personalizar las rutas de atributo
El modelo de aplicación es un modelo de objetos creado durante el inicio con todos los metadatos
utilizados por MVC para enrutar y ejecutar las acciones. El modelo de aplicación incluye todos los datos
recopilados de los atributos de ruta (a través de IRouteTemplateProvider ). Puede escribir convenciones
para modificar el modelo de aplicación en el momento del inicio y personalizar el comportamiento del
enrutamiento. En esta sección se muestra un ejemplo sencillo de personalización del enrutamiento
mediante el modelo de aplicación.
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using System.Linq;
using System.Text;
public class NamespaceRoutingConvention : IControllerModelConvention
{
private readonly string _baseNamespace;

public NamespaceRoutingConvention(string baseNamespace)


{
_baseNamespace = baseNamespace;
}

public void Apply(ControllerModel controller)


{
var hasRouteAttributes = controller.Selectors.Any(selector =>
selector.AttributeRouteModel != null);
if (hasRouteAttributes)
{
// This controller manually defined some routes, so treat this
// as an override and not apply the convention here.
return;
}

// Use the namespace and controller name to infer a route for the controller.
//
// Example:
//
// controller.ControllerTypeInfo -> "My.Application.Admin.UsersController"
// baseNamespace -> "My.Application"
//
// template => "Admin/[controller]"
//
// This makes your routes roughly line up with the folder structure of your project.
//
var namespc = controller.ControllerType.Namespace;
if (namespc == null)
return;
var template = new StringBuilder();
template.Append(namespc, _baseNamespace.Length + 1,
namespc.Length - _baseNamespace.Length - 1);
template.Replace('.', '/');
template.Append("/[controller]");

foreach (var selector in controller.Selectors)


{
selector.AttributeRouteModel = new AttributeRouteModel()
{
Template = template.ToString()
};
}
}
}

Enrutamiento mixto: enrutamiento mediante atributos frente a


enrutamiento convencional
Las aplicaciones MVC pueden combinar el uso de enrutamiento convencional y enrutamiento mediante
atributos. Es habitual usar las rutas convencionales para controladores que sirven páginas HTML para
los exploradores, y el enrutamiento mediante atributos para los controladores que sirven las API de
REST.
Las acciones se enrutan bien mediante convención o bien mediante atributos. Colocar una ruta en el
controlador o la acción hace que se enrute mediante atributos. Las acciones que definen rutas de atributo
no se pueden alcanzar a través de las rutas convencionales y viceversa. Cualquier atributo de ruta en el
controlador hace que todas las acciones del controlador se enruten mediante atributos.

NOTE
Lo que distingue los dos tipos de sistemas de enrutamiento es el proceso que se aplica después de que una
dirección URL coincida con una plantilla de ruta. En el enrutamiento convencional, los valores de ruta de la
coincidencia se usan para elegir la acción y el controlador en una tabla de búsqueda de todas las acciones
enrutadas convencionales. En el enrutamiento mediante atributos, cada plantilla ya está asociada a una acción y
no es necesaria ninguna otra búsqueda.

Generación de direcciones URL


Las aplicaciones MVC pueden usar características de generación de direcciones URL de enrutamiento
para generar vínculos URL a las acciones. La generación de direcciones URL elimina las direcciones URL
codificadas de forma rígida, por lo que el código es más compacto y fácil de mantener. Esta sección se
centra en las características de generación de direcciones URL proporcionadas por MVC y solo aborda
los conceptos básicos de su funcionamiento. Consulte Enrutamiento para obtener una descripción
detallada de la generación de direcciones URL.
La interfaz IUrlHelper es la pieza subyacente de la infraestructura entre MVC y el enrutamiento para la
generación de direcciones URL. Encontrará una instancia de IUrlHelper disponible a través de la
propiedad Url en los controladores, las vistas y los componentes de vista.
En este ejemplo, la interfaz IUrlHelper se usa a través de la propiedad Controller.Url para generar una
dirección URL a otra acción.

using Microsoft.AspNetCore.Mvc;

public class UrlGenerationController : Controller


{
public IActionResult Source()
{
// Generates /UrlGeneration/Destination
var url = Url.Action("Destination");
return Content($"Go check out {url}, it's really great.");
}

public IActionResult Destination()


{
return View();
}
}

Si la aplicación está usando la ruta convencional predeterminada, el valor de la variable url será la
cadena de ruta de dirección URL /UrlGeneration/Destination . Esta ruta de dirección URL se crea por
enrutamiento mediante la combinación de los valores de ruta de la solicitud actual (valores de ambiente)
con los valores pasados a Url.Action , y sustituyendo esos valores en la plantilla de ruta:

ambient values: { controller = "UrlGeneration", action = "Source" }


values passed to Url.Action: { controller = "UrlGeneration", action = "Destination" }
route template: {controller}/{action}/{id?}

result: /UrlGeneration/Destination

El valor de cada uno de los parámetros de ruta incluidos en la plantilla de ruta se sustituye por nombres
que coincidan con los valores y los valores de ambiente. Si un parámetro de ruta no tiene un valor, puede
utilizar un valor predeterminado en caso de tenerlo, o se puede omitir si es opcional (como en el caso de
id en este ejemplo). La generación de direcciones URL producirá un error si cualquiera de los
parámetros de ruta necesarios no tiene un valor correspondiente. Si se produce un error en la
generación de direcciones URL para una ruta, se prueba con la ruta siguiente hasta que se hayan
probado todas las rutas o se encuentra una coincidencia.
En el ejemplo anterior de Url.Action se supone que el enrutamiento es convencional, pero la
generación de direcciones URL funciona del mismo modo con el enrutamiento mediante atributos, si
bien los conceptos son diferentes. En el enrutamiento convencional, los valores de ruta se usan para
expandir una plantilla; los valores de ruta de controller y action suelen aparecer en esa plantilla. Esto
es válido porque las direcciones URL que coinciden con el enrutamiento se adhieren a una convención.
En el enrutamiento mediante atributos, no está permitido que los valores de ruta de controller y
action aparezcan en la plantilla. En vez de eso, se utilizan para buscar la plantilla que se va a usar.

Este ejemplo utiliza la enrutamiento mediante atributos:

// In Startup class
public void Configure(IApplicationBuilder app)
{
app.UseMvc();
}

using Microsoft.AspNetCore.Mvc;

public class UrlGenerationController : Controller


{
[HttpGet("")]
public IActionResult Source()
{
var url = Url.Action("Destination"); // Generates /custom/url/to/destination
return Content($"Go check out {url}, it's really great.");
}

[HttpGet("custom/url/to/destination")]
public IActionResult Destination() {
return View();
}
}

MVC genera una tabla de búsqueda de todas las acciones enrutadas mediante atributos y hará coincidir
los valores controller y action para seleccionar la plantilla de ruta que se usará para la generación de
direcciones URL. En el ejemplo anterior se genera custom/url/to/destination .
Generación de direcciones URL por nombre de acción
Url.Action ( IUrlHelper . Action ) y todas las sobrecargas relacionadas se basan en la idea de
especificar el destino del vínculo mediante un nombre de controlador y un nombre de acción.

NOTE
Cuando se usa Url.Action , se especifican los valores actuales de la ruta para controller y action . Los
valores de controller y action forman parte tanto de los valores de ambiente ycomo de los valores. El
método Url.Action siempre utiliza los valores actuales de action y controller y genera una ruta de
dirección URL que dirige a la acción actual.

Intentos de enrutamiento para utilizar los valores en los valores de ambiente para rellenar la información
que no se proporcionó al generar una dirección URL. Al utilizar una ruta como {a}/{b}/{c}/{d} y los
valores de ambiente { a = Alice, b = Bob, c = Carol, d = David } , el enrutamiento tiene suficiente
información para generar una dirección URL sin ningún valor adicional, puesto que todos los
parámetros de ruta tienen un valor. Si se agregó el valor { d = Donovan } , el valor { d = David } se
ignorará y la ruta de dirección URL generada será Alice/Bob/Carol/Donovan .

WARNING
Las rutas de dirección URL son jerárquicas. En el ejemplo anterior, si se agrega el valor { c = Cheryl } , ambos
valores { c = Carol, d = David } se ignorarán. En este caso ya no tenemos un valor para d y se producirá
un error en la generación de direcciones URL. Debe especificar el valor deseado de c y d . Es probable que este
problema se produzca con la ruta predeterminada ( {controller}/{action}/{id?} ), pero raramente encontrará
este comportamiento en la práctica, dado que Url.Action especificará siempre de manera explícita un valor
controller y action .

Las sobrecargas más largas de Url.Action también toman un objeto de valores de ruta adicional para
proporcionar valores para parámetros de ruta distintos de controller y action . Normalmente verá
esto utilizado con id , como Url.Action("Buy", "Products", new { id = 17 }) . Por convención, el objeto
de valores de ruta normalmente es un objeto de tipo anónimo, pero también puede ser IDictionary<> o
un objeto .NET estándar. Los valores de ruta adicionales que no coinciden con los parámetros de ruta se
colocan en la cadena de consulta.

using Microsoft.AspNetCore.Mvc;

public class TestController : Controller


{
public IActionResult Index()
{
// Generates /Products/Buy/17?color=red
var url = Url.Action("Buy", "Products", new { id = 17, color = "red" });
return Content(url);
}
}

TIP
Para crear una dirección URL absoluta, use una sobrecarga que acepte protocol :
Url.Action("Buy", "Products", new { id = 17 }, protocol: Request.Scheme)

Generación de direcciones URL por ruta


En el código anterior se pasó el nombre de acción y de controlador para generar una dirección URL.
IUrlHelper también proporciona la familia de métodos Url.RouteUrl . Estos métodos son similares a
Url.Action , pero no copian los valores actuales de action y controller en los valores de ruta. Lo más
común es especificar un nombre de ruta para utilizar una ruta específica y generar la dirección URL, por
lo general sin especificar un nombre de acción o controlador.
using Microsoft.AspNetCore.Mvc;

public class UrlGenerationController : Controller


{
[HttpGet("")]
public IActionResult Source()
{
var url = Url.RouteUrl("Destination_Route"); // Generates /custom/url/to/destination
return Content($"See {url}, it's really great.");
}

[HttpGet("custom/url/to/destination", Name = "Destination_Route")]


public IActionResult Destination() {
return View();
}
}

Generación de direcciones URL en HTML


IHtmlHelper proporciona los métodos de HtmlHelper Html.BeginForm y Html.ActionLink para generar
elementos <form> y <a> , respectivamente. Estos métodos utilizan el método Url.Action para generar
una dirección URL y aceptan argumentos similares. Los métodos Url.RouteUrl complementarios de
HtmlHelper son Html.BeginRouteForm y Html.RouteLink , cuya funcionalidad es similar.

Las TagHelper generan direcciones URL a través de la TagHelper form y la TagHelper <a> . Ambos usan
IUrlHelper para su implementación. Consulte Trabajar con formularios para obtener más información.

Dentro de las vistas, IUrlHelper está disponible a través de la propiedad Url para una generación de
direcciones URL ad hoc no cubierta por los pasos anteriores.
Generar direcciones URL en los resultados de acción
Los ejemplos anteriores han demostrado el uso de IUrlHelper en un controlador, aunque el uso más
común de un controlador consiste en generar una dirección URL como parte de un resultado de acción.
Las clases base ControllerBase y Controller proporcionan métodos de conveniencia para los
resultados de acción que hacen referencia a otra acción. Un uso típico es redirigir después de aceptar la
entrada del usuario.

public Task<IActionResult> Edit(int id, Customer customer)


{
if (ModelState.IsValid)
{
// Update DB with new details.
return RedirectToAction("Index");
}
}

Los patrones de diseño Factory Method de resultados de acción siguen un patrón similar a los métodos
en IUrlHelper .
Caso especial para rutas convencionales dedicadas
El enrutamiento convencional puede usar un tipo especial de definición de ruta denominada ruta
convencional dedicada. En el ejemplo siguiente, la ruta llamada blog es una ruta convencional
dedicada.
app.UseMvc(routes =>
{
routes.MapRoute("blog", "blog/{*article}",
defaults: new { controller = "Blog", action = "Article" });
routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
});

Con estas definiciones de ruta, Url.Action("Index", "Home") generará la ruta de dirección URL / con la
ruta default . Pero, ¿por qué? Se puede suponer que los valores de ruta
{ controller = Home, action = Index } son suficientes para generar una dirección URL utilizando blog ,
con el resultado /blog?action=Index&controller=Home .
Las rutas convencionales dedicadas se basan en un comportamiento especial de los valores
predeterminados que no tienen un parámetro de ruta correspondiente que impida que la ruta sea
"demasiado expansiva" con la generación de direcciones URL. En este caso, los valores predeterminados
son { controller = Blog, action = Article } , y ni controller ni action aparecen como un parámetro
de ruta. Cuando el enrutamiento realiza la generación de direcciones URL, los valores proporcionados
deben coincidir con los valores predeterminados. La generación de direcciones URL con blog producirá
un error porque los valores { controller = Home, action = Index } no coinciden con
{ controller = Blog, action = Article } . Después, el enrutamiento vuelve para probar default ,
operación que se realiza correctamente.

Áreas
Las áreas son una característica de MVC que se usa para organizar funciones relacionadas en un grupo
como un espacio de nombres de enrutamiento independiente (para acciones de controlador) y una
estructura de carpetas (para las vistas). La utilización de áreas permite que una aplicación tenga varios
controladores con el mismo nombre, siempre y cuando tengan áreas diferentes. El uso de áreas crea una
jerarquía para el enrutamiento mediante la adición de otro parámetro de ruta, area , a controller y
action . En esta sección veremos la interacción entre el enrutamiento y las áreas. Consulte Áreas para
obtener más información sobre cómo se utilizan las áreas con las vistas.
En el ejemplo siguiente se configura MVC para usar la ruta predeterminada convencional y una ruta de
área para un área denominada Blog :

app.UseMvc(routes =>
{
routes.MapAreaRoute("blog_route", "Blog",
"Manage/{controller}/{action}/{id?}");
routes.MapRoute("default_route", "{controller}/{action}/{id?}");
});

Cuando coincide con una ruta de dirección URL como /Manage/Users/AddUser , la primera ruta generará
los valores de ruta { area = Blog, controller = Users, action = AddUser } . El valor de ruta area se
genera con un valor predeterminado para area . De hecho, la ruta creada por MapAreaRoute es
equivalente a la siguiente:

app.UseMvc(routes =>
{
routes.MapRoute("blog_route", "Manage/{controller}/{action}/{id?}",
defaults: new { area = "Blog" }, constraints: new { area = "Blog" });
routes.MapRoute("default_route", "{controller}/{action}/{id?}");
});
MapAreaRoute utiliza el nombre de área proporcionado, que en este caso es Blog , para crear una ruta
con un valor predeterminado y una restricción para area . El valor predeterminado garantiza que la ruta
siempre produce { area = Blog, ... } ; la restricción requiere el valor { area = Blog, ... } para la
generación de la dirección URL.

TIP
El enrutamiento convencional depende del orden. En general, las rutas con áreas deben colocarse antes en la tabla
de rutas, ya que son más específicas que las rutas sin un área.

En el ejemplo anterior, los valores de ruta coincidirían con la acción siguiente:

using Microsoft.AspNetCore.Mvc;

namespace MyApp.Namespace1
{
[Area("Blog")]
public class UsersController : Controller
{
public IActionResult AddUser()
{
return View();
}
}
}

AreaAttribute es lo que denota un controlador como parte de un área. Se dice que este controlador está
en el área Blog . Los controladores sin un atributo [Area] no son miembros de ningún área y no
coincidirán cuando el enrutamiento proporcione el valor de ruta area . En el ejemplo siguiente, solo el
primer controlador enumerado puede coincidir con los valores de ruta
{ area = Blog, controller = Users, action = AddUser } .

using Microsoft.AspNetCore.Mvc;

namespace MyApp.Namespace1
{
[Area("Blog")]
public class UsersController : Controller
{
public IActionResult AddUser()
{
return View();
}
}
}
using Microsoft.AspNetCore.Mvc;

namespace MyApp.Namespace2
{
// Matches { area = Zebra, controller = Users, action = AddUser }
[Area("Zebra")]
public class UsersController : Controller
{
public IActionResult AddUser()
{
return View();
}
}
}

using Microsoft.AspNetCore.Mvc;

namespace MyApp.Namespace3
{
// Matches { area = string.Empty, controller = Users, action = AddUser }
// Matches { area = null, controller = Users, action = AddUser }
// Matches { controller = Users, action = AddUser }
public class UsersController : Controller
{
public IActionResult AddUser()
{
return View();

}
}
}

NOTE
En aras de la exhaustividad, aquí se muestra el espacio de nombres de cada controlador; en caso contrario, los
controladores tendrían un conflicto de nomenclatura y generarían un error del compilador. Los espacios de
nombres de clase no tienen ningún efecto en el enrutamiento de MVC.

Los dos primeros controladores son miembros de las áreas y solo coinciden cuando el valor de ruta
area proporciona su respectivo nombre de área. El tercer controlador no es miembro de ningún área y
solo puede coincidir cuando el enrutamiento no proporciona ningún valor para area .

NOTE
En términos de búsqueda de coincidencias de ningún valor, la ausencia del valor area es igual que si el valor de
area fuese null o una cadena vacía.

Al ejecutar una acción en un área, el valor de ruta para area estará disponible como un valor de
ambiente para que el enrutamiento pueda usarlo en la generación de direcciones URL. Esto significa
que, de forma predeterminada, las áreas actúan de forma adhesiva para la generación de direcciones
URL, tal como se muestra en el ejemplo siguiente.
app.UseMvc(routes =>
{
routes.MapAreaRoute("duck_route", "Duck",
"Manage/{controller}/{action}/{id?}");
routes.MapRoute("default", "Manage/{controller=Home}/{action=Index}/{id?}");
});

using Microsoft.AspNetCore.Mvc;

namespace MyApp.Namespace4
{
[Area("Duck")]
public class UsersController : Controller
{
public IActionResult GenerateURLInArea()
{
// Uses the 'ambient' value of area
var url = Url.Action("Index", "Home");
// returns /Manage
return Content(url);
}

public IActionResult GenerateURLOutsideOfArea()


{
// Uses the empty value for area
var url = Url.Action("Index", "Home", new { area = "" });
// returns /Manage/Home/Index
return Content(url);
}
}
}

Descripción de IActionConstraint
NOTE
En esta sección se analiza con mayor profundidad el funcionamiento interno del marco y la elección por parte de
MVC de la acción que se va a ejecutar. Una aplicación típica no necesitará un IActionConstraint personalizado.

Es probable que ya haya usado IActionConstraint incluso si no está familiarizado con la interfaz. El
atributo [HttpGet] y los atributos [Http-VERB] similares implementan IActionConstraint con el fin de
limitar la ejecución de un método de acción.

public class ProductsController : Controller


{
[HttpGet]
public IActionResult Edit() { }

public IActionResult Edit(...) { }


}

Adoptando la ruta convencional predeterminada, la ruta de dirección URL /Products/Edit generaría los
valores { controller = Products, action = Edit } , que coincidirían con ambas acciones que se
muestran aquí. En terminología de IActionConstraint , diríamos que ambas acciones se consideran
candidatas, puesto que las dos coinciden con los datos de ruta.
Cuando HttpGetAttribute se ejecute, dirá que Edit() es una coincidencia para GET, pero no para
cualquier otro verbo HTTP. La acción Edit(...) no tiene ninguna restricción definida, por lo que
coincidirá con cualquier verbo HTTP. Con POST , solamente Edit(...) coincide. Pero con GET ambas
acciones pueden coincidir. No obstante, una acción con IActionConstraint siempre se considera mejor
que una acción sin dicha restricción. Por tanto, como Edit() tiene [HttpGet] , se considera más
específica y se seleccionará si ambas acciones pueden coincidir.
Conceptualmente, IActionConstraint es una forma de sobrecarga, pero en lugar de sobrecargar
métodos con el mismo nombre, la sobrecarga se produce entre acciones que coinciden con la misma
dirección URL. El enrutamiento mediante atributos también utiliza IActionConstraint y puede dar lugar
a que acciones de diferentes controladores se consideren candidatas.
Implementación de IActionConstraint
La manera más sencilla de implementar IActionConstraint consiste en crear una clase derivada de
System.Attribute y colocarla en las acciones y los controladores. MVC detectará automáticamente
cualquier IActionConstraint que se aplique como atributo. El modelo de aplicaciones es quizá el
enfoque más flexible para la aplicación de restricciones, puesto que permite metaprogramar cómo se
aplican.
En el ejemplo siguiente, una restricción elige una acción según un código de país de los datos de ruta. El
ejemplo completo se encuentra en GitHub.

public class CountrySpecificAttribute : Attribute, IActionConstraint


{
private readonly string _countryCode;

public CountrySpecificAttribute(string countryCode)


{
_countryCode = countryCode;
}

public int Order


{
get
{
return 0;
}
}

public bool Accept(ActionConstraintContext context)


{
return string.Equals(
context.RouteContext.RouteData.Values["country"].ToString(),
_countryCode,
StringComparison.OrdinalIgnoreCase);
}
}

Usted es responsable de implementar el método Accept y elegir un valor para "Order" para que la
restricción se ejecute. En este caso, el método Accept devuelve true para denotar que la acción es una
coincidencia cuando el valor de ruta country coincide. Esto difiere de RouteValueAttribute en que
permite volver a una acción sin atributos. El ejemplo muestra que si se define una acción en-US ,
entonces un código de país como fr-FR recurriría a un controlador más genérico que no tenga
[CountrySpecific(...)] aplicado.

La propiedad Order decide de qué fase forma parte la restricción. Restricciones de acciones que se
ejecutan en grupos basados en la Order . Por ejemplo, todos los atributos del método HTTP
proporcionados por el marco utilizan el mismo valor Order para que se ejecuten en la misma fase.
Puede tener las fases que sean necesarias para implementar las directivas deseadas.
TIP
Para decidir el valor de Order , piense si la restricción se debería o no aplicar antes que los métodos HTTP. Los
números más bajos se ejecutan primero.
Cargas de archivos en ASP.NET Core
25/06/2018 • 13 minutes to read • Edit Online

Por Steve Smith


Entre las acciones que se pueden realizar en ASP.NET MVC está la de cargar uno o más archivos por medio de un
sencillo procedimiento de enlace de modelos (archivos más pequeños) o del streaming (archivos de mayor
tamaño).
Ver o descargar el ejemplo desde GitHub

Cargar archivos pequeños por medio del enlace de modelos


Para cargar archivos pequeños, se puede usar un formulario HTML de varias partes o crear una solicitud POST
con JavaScript. Este es un formulario de ejemplo en el que se usa Razor y que admite varios archivos cargados:

<form method="post" enctype="multipart/form-data" asp-controller="UploadFiles" asp-action="Index">


<div class="form-group">
<div class="col-md-10">
<p>Upload one or more files using this form:</p>
<input type="file" name="files" multiple />
</div>
</div>
<div class="form-group">
<div class="col-md-10">
<input type="submit" value="Upload" />
</div>
</div>
</form>

Para admitir las cargas de archivos, los formularios HTML deben especificar un tipo enctype de
multipart/form-data . El elemento de entrada files de arriba admite la carga de varios archivos. Para admitir un
único archivo cargado, solo hay que omitir el atributo multiple de este elemento de entrada. El marcado anterior
se muestra así en un explorador:

Se puede tener acceso a los archivos individuales cargados en el servidor a través del enlace de modelos, por
medio de la interfaz IFormFile. IFormFile tiene la siguiente estructura:
public interface IFormFile
{
string ContentType { get; }
string ContentDisposition { get; }
IHeaderDictionary Headers { get; }
long Length { get; }
string Name { get; }
string FileName { get; }
Stream OpenReadStream();
void CopyTo(Stream target);
Task CopyToAsync(Stream target, CancellationToken cancellationToken = null);
}

WARNING
No se base o confíe en la propiedad FileName sin validarla. La propiedad FileName solo se debe usar con fines ilustrativos.

Al cargar archivos por medio del enlace de modelos y la interfaz IFormFile , el método de acción puede aceptar
bien un solo IFormFile , bien un IEnumerable<IFormFile> (o List<IFormFile> ) que represente varios archivos. En el
siguiente ejemplo se itera por uno o varios archivos cargados, estos se guardan en el sistema de archivos local y se
devuelve el número total y el tamaño de los archivos cargados.
Advertencia: el siguiente código utiliza GetTempFileName , que produce una IOException si no se crean más de
65535 archivos sin eliminar los archivos temporales anteriores. Debe eliminar los archivos temporales o usar una
aplicación real GetTempPath y GetRandomFileName para crear nombres de archivo temporal. El límite de 65535
archivos es por servidor, por lo que puede usar otra aplicación en el servidor de seguridad de todos los archivos de
65535.

[HttpPost("UploadFiles")]
public async Task<IActionResult> Post(List<IFormFile> files)
{
long size = files.Sum(f => f.Length);

// full path to file in temp location


var filePath = Path.GetTempFileName();

foreach (var formFile in files)


{
if (formFile.Length > 0)
{
using (var stream = new FileStream(filePath, FileMode.Create))
{
await formFile.CopyToAsync(stream);
}
}
}

// process uploaded files


// Don't rely on or trust the FileName property without validation.

return Ok(new { count = files.Count, size, filePath});


}

Los archivos que se cargan usando la técnica IFormFile se almacenan en búfer en memoria o en disco en el
servidor web antes de procesarse. Dentro del método de acción, se puede tener acceso al contenido de IFormFile
como una secuencia. Aparte de al sistema de archivos local, los archivos se pueden transmitir por streaming
también a Azure Blob Storage o a Entity Framework.
Para almacenar datos de archivo binario en una base de datos con Entity Framework, defina una propiedad de tipo
byte[] en la entidad:

public class ApplicationUser : IdentityUser


{
public byte[] AvatarImage { get; set; }
}

Especifique una propiedad ViewModel de tipo IFormFile :

public class RegisterViewModel


{
// other properties omitted

public IFormFile AvatarImage { get; set; }


}

NOTE
IFormFile se puede usar directamente como un parámetro de método de acción o como una propiedad ViewModel, tal y
como se aprecia arriba.

Copie IFormFile en una secuencia y guárdela en la matriz de bytes:

// POST: /Account/Register
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Register(RegisterViewModel model)
{
ViewData["ReturnUrl"] = returnUrl;
if (ModelState.IsValid)
{
var user = new ApplicationUser {
UserName = model.Email,
Email = model.Email
};
using (var memoryStream = new MemoryStream())
{
await model.AvatarImage.CopyToAsync(memoryStream);
user.AvatarImage = memoryStream.ToArray();
}
// additional logic omitted

// Don't rely on or trust the model.AvatarImage.FileName property


// without validation.
}

NOTE
Tenga cuidado al almacenar los datos binarios en bases de datos relacionales, ya que esto puede repercutir adversamente en
el rendimiento.

Cargar archivos grandes con streaming


Si el tamaño o la frecuencia de las cargas de archivos están causando problemas de recursos en la aplicación,
considere la posibilidad de usar el streaming para cargar archivos en lugar de almacenarlos completamente en el
búfer, como ocurre con el enlace de modelos descrito anteriormente. Usar IFormFile y el enlace de modelos es
una solución mucho más sencilla, mientras que en el streaming hay que realizar una serie de pasos para
implementarlo correctamente.

NOTE
Cualquier archivo individual almacenado en búfer con un tamaño superior a 64 KB se trasladará desde la memoria RAM a un
archivo temporal en el disco en el servidor. Los recursos (disco, memoria RAM) que se usan en las cargas de archivos
dependen de la cantidad y del tamaño de las cargas de archivos que se realizan simultáneamente. En el streaming lo
importante no es el rendimiento, sino la escala. Si se intentan almacenar demasiadas cargas en búfer, el sitio se bloqueará
cuando se quede sin memoria o sin espacio en disco.

En el siguiente ejemplo se describe cómo usar JavaScript/Angular para transmitir por streaming a una acción de
controlador. El token de antifalsificación del archivo se genera por medio de un atributo de filtro personalizado y se
pasa en encabezados HTTP, en lugar de en el cuerpo de la solicitud. Dado que el método de acción procesa los
datos cargados directamente, el enlace de modelos se deshabilita por otro filtro. Dentro de la acción, el contenido
del formulario se lee usando un MultipartReader (que lee cada MultipartSection individual), de forma que el
archivo se procesa o el contenido se almacena, según corresponda. Una vez que se han leído todas las secciones, la
acción realiza su enlace de modelos particular.
La acción inicial carga el formulario y guarda un token de antifalsificación en una cookie (a través del atributo
GenerateAntiforgeryTokenCookieForAjax ):

[HttpGet]
[GenerateAntiforgeryTokenCookieForAjax]
public IActionResult Index()
{
return View();
}

El atributo usa la compatibilidad de antifalsificación integrada de ASP.NET Core para establecer una cookie con un
token de solicitud:

public class GenerateAntiforgeryTokenCookieForAjaxAttribute : ActionFilterAttribute


{
public override void OnActionExecuted(ActionExecutedContext context)
{
var antiforgery = context.HttpContext.RequestServices.GetService<IAntiforgery>();

// We can send the request token as a JavaScript-readable cookie,


// and Angular will use it by default.
var tokens = antiforgery.GetAndStoreTokens(context.HttpContext);
context.HttpContext.Response.Cookies.Append(
"XSRF-TOKEN",
tokens.RequestToken,
new CookieOptions() { HttpOnly = false });
}
}

Angular pasa automáticamente un token de antifalsificación en un encabezado de solicitud denominado


X-XSRF-TOKEN . La aplicación ASP.NET Core MVC está definida para hacer referencia a este encabezado en su
configuración en Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
// Angular's default header name for sending the XSRF token.
services.AddAntiforgery(options => options.HeaderName = "X-XSRF-TOKEN");

services.AddMvc();
}

El atributo DisableFormValueModelBinding , mostrado abajo, se usa para deshabilitar el enlace de modelos en el


método de acción Upload .

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
{
public void OnResourceExecuting(ResourceExecutingContext context)
{
var factories = context.ValueProviderFactories;
factories.RemoveType<FormValueProviderFactory>();
factories.RemoveType<JQueryFormValueProviderFactory>();
}

public void OnResourceExecuted(ResourceExecutedContext context)


{
}
}

Como el enlace de modelos está deshabilitado, el método de acción Upload no acepta parámetros. Funciona
directamente con la propiedad Request de ControllerBase . Se usa un elemento MultipartReader para leer cada
sección. El archivo se guarda con un nombre de archivo GUID y los datos de clave/valor se almacenan en un
KeyValueAccumulator . Una vez que todas las secciones se han leído, el contenido de KeyValueAccumulator se usa
para enlazar los datos del formulario a un tipo de modelo.
Abajo mostramos el método Upload completo:
Advertencia: el siguiente código utiliza GetTempFileName , que produce una IOException si no se crean más de
65535 archivos sin eliminar los archivos temporales anteriores. Debe eliminar los archivos temporales o usar una
aplicación real GetTempPath y GetRandomFileName para crear nombres de archivo temporal. El límite de 65535
archivos es por servidor, por lo que puede usar otra aplicación en el servidor de seguridad de todos los archivos de
65535.

// 1. Disable the form value model binding here to take control of handling
// potentially large files.
// 2. Typically antiforgery tokens are sent in request body, but since we
// do not want to read the request body early, the tokens are made to be
// sent via headers. The antiforgery token filter first looks for tokens
// in the request header and then falls back to reading the body.
[HttpPost]
[DisableFormValueModelBinding]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Upload()
{
if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
{
return BadRequest($"Expected a multipart request, but got {Request.ContentType}");
}

// Used to accumulate all the form url encoded key value pairs in the
// request.
var formAccumulator = new KeyValueAccumulator();
string targetFilePath = null;
var boundary = MultipartRequestHelper.GetBoundary(
MediaTypeHeaderValue.Parse(Request.ContentType),
_defaultFormOptions.MultipartBoundaryLengthLimit);
var reader = new MultipartReader(boundary, HttpContext.Request.Body);

var section = await reader.ReadNextSectionAsync();


while (section != null)
{
ContentDispositionHeaderValue contentDisposition;
var hasContentDispositionHeader = ContentDispositionHeaderValue.TryParse(section.ContentDisposition,
out contentDisposition);

if (hasContentDispositionHeader)
{
if (MultipartRequestHelper.HasFileContentDisposition(contentDisposition))
{
targetFilePath = Path.GetTempFileName();
using (var targetStream = System.IO.File.Create(targetFilePath))
{
await section.Body.CopyToAsync(targetStream);

_logger.LogInformation($"Copied the uploaded file '{targetFilePath}'");


}
}
else if (MultipartRequestHelper.HasFormDataContentDisposition(contentDisposition))
{
// Content-Disposition: form-data; name="key"
//
// value

// Do not limit the key name length here because the


// multipart headers length limit is already in effect.
var key = HeaderUtilities.RemoveQuotes(contentDisposition.Name);
var encoding = GetEncoding(section);
using (var streamReader = new StreamReader(
section.Body,
encoding,
detectEncodingFromByteOrderMarks: true,
bufferSize: 1024,
leaveOpen: true))
{
// The value length limit is enforced by MultipartBodyLengthLimit
var value = await streamReader.ReadToEndAsync();
if (String.Equals(value, "undefined", StringComparison.OrdinalIgnoreCase))
{
value = String.Empty;
}
formAccumulator.Append(key, value);

if (formAccumulator.ValueCount > _defaultFormOptions.ValueCountLimit)


{
throw new InvalidDataException($"Form key count limit
{_defaultFormOptions.ValueCountLimit} exceeded.");
}
}
}
}

// Drains any remaining section body that has not been consumed and
// reads the headers for the next section.
section = await reader.ReadNextSectionAsync();
}

// Bind form data to a model


var user = new User();
var formValueProvider = new FormValueProvider(
BindingSource.Form,
new FormCollection(formAccumulator.GetResults()),
CultureInfo.CurrentCulture);
CultureInfo.CurrentCulture);

var bindingSuccessful = await TryUpdateModelAsync(user, prefix: "",


valueProvider: formValueProvider);
if (!bindingSuccessful)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
}

var uploadedData = new UploadedData()


{
Name = user.Name,
Age = user.Age,
Zipcode = user.Zipcode,
FilePath = targetFilePath
};
return Json(uploadedData);
}

Solución de problemas
Aquí incluimos algunos problemas comunes que pueden surgir al cargar archivos, así como sus posibles
soluciones.
Error "No encontrado" inesperado en IIS
El siguiente error indica que la carga de archivos supera el valor de maxAllowedContentLength configurado en el
servidor:

HTTP 404.13 - Not Found


The request filtering module is configured to deny a request that exceeds the request content length.

El valor predeterminado es 30000000 , que son alrededor de 28,6 MB. Este valor se puede personalizar editando el
archivo web.config:

<system.webServer>
<security>
<requestFiltering>
<!-- This will handle requests up to 50MB -->
<requestLimits maxAllowedContentLength="52428800" />
</requestFiltering>
</security>
</system.webServer>

Esto solo ocurre en IIS; este comportamiento no sucede de forma predeterminada cuando los archivos se
hospedan en Kestrel. Para más información, vea Request Limits <requestLimits> (Límites de solicitudes).
Excepción de referencia nula con IFormFile
Si el controlador acepta archivos cargados con IFormFile , pero ve que el valor siempre es null, confirme que en el
formulario HTML se especifica un valor enctype de multipart/form-data . Si este atributo no está establecido en el
elemento <form> , la carga de archivos no se llevará a cabo y cualquier argumento IFormFile enlazado será nulo.
Inserción de dependencias en controladores en
ASP.NET Core
25/06/2018 • 9 minutes to read • Edit Online

Por Steve Smith


Los controladores de ASP.NET Core MVC deben solicitar sus dependencias explícitamente a través de sus
constructores. En algunos casos, puede haber acciones específicas de controlador que requieran un servicio y
quizá no tenga sentido realizar la solicitud en el nivel de controlador. En este caso, también puede insertar un
servicio como un parámetro del método de acción.
Vea o descargue el código de ejemplo (cómo descargarlo)

Inserción de dependencias
La inserción de dependencias es una técnica que sigue el principio de inversión de dependencias, lo que permite
que las aplicaciones consten de módulos de acoplamiento flexible. ASP.NET Core tiene compatibilidad integrada
para inserción de dependencias, lo que facilita las tareas de prueba y mantenimiento de las aplicaciones.

Inserción de constructores
La compatibilidad integrada de ASP.NET Core con la inserción de dependencias basada en constructores se
extiende a los controladores MVC. Simplemente con agregar un tipo de servicio al controlador como un
parámetro de constructor, ASP.NET Core intentará resolver ese tipo mediante su contenedor de servicios
integrado. Normalmente, los servicios se definen mediante interfaces, aunque no siempre es así. Por ejemplo, si
la aplicación tiene lógica de negocios que depende de la hora actual, también se puede insertar un servicio que
recupera la hora (en lugar de codificarla de forma rígida), lo que permitiría superar las pruebas en
implementaciones que usan una hora determinada.

using System;

namespace ControllerDI.Interfaces
{
public interface IDateTime
{
DateTime Now { get; }
}
}

Implementar una interfaz como esta para que utilice el reloj del sistema en tiempo de ejecución no es nada
complicado:
using System;
using ControllerDI.Interfaces;

namespace ControllerDI.Services
{
public class SystemDateTime : IDateTime
{
public DateTime Now
{
get { return DateTime.Now; }
}
}
}

Teniendo esto implementado, podemos utilizar el servicio en el controlador. En este caso, hemos agregado lógica
al método Index de HomeController para presentar un saludo al usuario en función de la hora del día.

using ControllerDI.Interfaces;
using Microsoft.AspNetCore.Mvc;

namespace ControllerDI.Controllers
{
public class HomeController : Controller
{
private readonly IDateTime _dateTime;

public HomeController(IDateTime dateTime)


{
_dateTime = dateTime;
}

public IActionResult Index()


{
var serverTime = _dateTime.Now;
if (serverTime.Hour < 12)
{
ViewData["Message"] = "It's morning here - Good Morning!";
}
else if (serverTime.Hour < 17)
{
ViewData["Message"] = "It's afternoon here - Good Afternoon!";
}
else
{
ViewData["Message"] = "It's evening here - Good Evening!";
}
return View();
}
}
}

Si se ejecuta la aplicación ahora, probablemente se producirá un error:

An unhandled exception occurred while processing the request.

InvalidOperationException: Unable to resolve service for type 'ControllerDI.Interfaces.IDateTime' while


attempting to activate 'ControllerDI.Controllers.HomeController'.
Microsoft.Extensions.DependencyInjection.ActivatorUtilities.GetService(IServiceProvider sp, Type type, Type
requiredBy, Boolean isDefaultParameterRequired)

Este error se produce cuando no se ha configurado un servicio en el método ConfigureServices de nuestra clase
Startup . Para especificar que las solicitudes de IDateTime deben resolverse mediante una instancia de
SystemDateTime , agregue la línea resaltada en la siguiente lista a su método ConfigureServices :

public void ConfigureServices(IServiceCollection services)


{
// Add application services.
services.AddTransient<IDateTime, SystemDateTime>();
}

NOTE
Este servicio en particular podría implementarse mediante cualquiera de las diversas opciones de duración ( Transient ,
Scoped o Singleton ). Consulte Dependency Injection (Inserción de dependencias) para ver cómo afectará al
comportamiento de su servicio cada una de estas opciones de ámbito.

Una vez que se ha configurado el servicio, al ejecutar la aplicación y navegar a la página principal se debería
mostrar el mensaje basado en la hora según lo esperado:

TIP
Vea Testing controller logic (Comprobación de la lógica de controlador) para obtener información sobre cómo solicitar
explícitamente dependencias http://deviq.com/explicit-dependencies-principle/ en controladores para facilitar la
comprobación de código.

La inserción de dependencias integrada de ASP.NET Core es compatible con tener un solo constructor para las
clases que soliciten servicios. Si se tiene más de un constructor, es posible recibir la siguiente excepción:

An unhandled exception occurred while processing the request.

InvalidOperationException: Multiple constructors accepting all given argument types have been found in type
'ControllerDI.Controllers.HomeController'. There should only be one applicable constructor.
Microsoft.Extensions.DependencyInjection.ActivatorUtilities.FindApplicableConstructor(Type instanceType,
Type[] argumentTypes, ConstructorInfo& matchingConstructor, Nullable`1[]& parameterMap)

Tal como indica el mensaje de error, tener un solo constructor corregiría el problema. También se puede
reemplazar la compatibilidad de inserción de dependencias predeterminada por una implementación de otros
fabricantes que sea compatible con varios constructores.

Inserción de acción con FromServices


A veces, un servicio solo es necesario para una acción en el controlador. En este caso, puede tener sentido
insertar el servicio como un parámetro en el método de acción. Para ello, se marca el parámetro con el atributo
[FromServices] , tal como se muestra a continuación:
public IActionResult About([FromServices] IDateTime dateTime)
{
ViewData["Message"] = "Currently on the server the time is " + dateTime.Now;

return View();
}

Acceso a la configuración desde un controlador


El acceso a la configuración de la aplicación o a los valores de configuración desde un controlador es un patrón
habitual. Este acceso debe utilizar el patrón de opciones que se describe en el artículo sobre configuración. Por lo
general, no es recomendable solicitar la configuración directamente desde el controlador mediante la inserción
de dependencias. Un enfoque más adecuado consiste en solicitar una instancia de IOptions<T> , donde T es la
clase de configuración que se necesita.
Para trabajar con el patrón de opciones, debe crear una clase como la siguiente que represente las opciones:

namespace ControllerDI.Model
{
public class SampleWebSettings
{
public string Title { get; set; }
public int Updates { get; set; }
}
}

A continuación, necesita configurar la aplicación para que use el modelo de opciones y agregar la clase de
configuración a la colección de servicios en ConfigureServices :
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("samplewebsettings.json");
Configuration = builder.Build();
}

public IConfigurationRoot Configuration { get; set; }

// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?
LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
// Required to use the Options<T> pattern
services.AddOptions();

// Add settings from configuration


services.Configure<SampleWebSettings>(Configuration);

// Uncomment to add settings from code


//services.Configure<SampleWebSettings>(settings =>
//{
// settings.Updates = 17;
//});

services.AddMvc();

// Add application services.


services.AddTransient<IDateTime, SystemDateTime>();
}

NOTE
En la lista anterior, se configura la aplicación para que lea la configuración de un archivo con formato JSON. La
configuración también se puede definir completamente en código, como se muestra en el código comentado anterior.
Consulte Configuración para conocer más opciones de configuración.

Una vez que haya especificado un objeto de configuración fuertemente tipado (en este caso, SampleWebSettings )
y lo haya agregado a la colección de servicios, podrá solicitarlo desde cualquier método de acción o controlador
mediante la solicitud de una instancia de IOptions<T> (en este caso, IOptions<SampleWebSettings> ). El código
siguiente muestra cómo se podría solicitar la configuración desde un controlador:

public class SettingsController : Controller


{
private readonly SampleWebSettings _settings;

public SettingsController(IOptions<SampleWebSettings> settingsOptions)


{
_settings = settingsOptions.Value;
}

public IActionResult Index()


{
ViewData["Title"] = _settings.Title;
ViewData["Updates"] = _settings.Updates;
return View();
}
}
Seguir el patrón de opciones permite desacoplar entre sí los valores y la configuración, y garantiza que el
controlador respete la separación de intereses, ya que no necesita saber cómo ni dónde encontrar la información
de configuración. También hace que sea más fácil realizar una prueba unitaria de la lógica del controlador, puesto
que no hay efecto static cling ni creación directa de instancias de clases de configuración dentro de la clase de
controlador.
Probar la lógica del controlador en ASP.NET Core
25/06/2018 • 27 minutes to read • Edit Online

Por Steve Smith


Los controladores de las aplicaciones ASP.NET MVC deben ser de tamaño reducido e ir enfocados a abordar
problemas de la interfaz de usuario. Los controladores de gran tamaño que tratan problemas no relacionados con
la interfaz de usuario son bastante más difíciles de probar y mantener.
Ver o descargar el ejemplo desde GitHub

Probar los controladores


Los controladores son una parte fundamental de cualquier aplicación ASP.NET Core MVC. Por tanto, debe tener la
seguridad de que se comportan según lo previsto en la aplicación. Las pruebas automatizadas pueden darle esta
seguridad, así como detectar errores antes de que lleguen a la fase producción. Es importante no asignar
responsabilidades innecesarias a los controladores y procurar que las pruebas se centran únicamente en las
responsabilidades del controlador.
La lógica de controlador debería ser mínima y no ir enfocada a cuestiones de infraestructura o lógica empresarial
(por ejemplo, el acceso a datos). Compruebe la lógica del controlador, no el marco. Compruebe el comportamiento
del controlador en función de las entradas válidas o no válidas. Compruebe las respuestas de controlador según el
resultado de la operación empresarial que realiza.
Estas son algunas de las responsabilidades habituales de los controladores:
Comprobar ModelState.IsValid
Devolver una respuesta de error si ModelState no es válido
Recuperar una entidad de negocio de la persistencia
Llevar a cabo una acción en la entidad empresarial
Guardar la entidad comercial para persistencia
Devolver un IActionResult apropiado

Pruebas unitarias
Las pruebas unitarias conllevan probar una parte de una aplicación de forma aislada con respecto a su
infraestructura y dependencias. Cuando se realizan pruebas unitarias de la lógica de controlador, solo se
comprueba el contenido de una única acción, no el comportamiento de sus dependencias o del marco en sí.
Cuando realice pruebas unitarias de sus acciones de controlador, asegúrese de que solo se centran en el
comportamiento. Una prueba unitaria de controlador evita tener que recurrir a elementos como los filtros, el
enrutamiento o el enlace de modelos. Al centrarse en comprobar solo una cosa, las pruebas unitarias suelen ser
fáciles de escribir y rápidas de ejecutar. Un conjunto de pruebas unitarias bien escrito se puede ejecutar con
frecuencia sin demasiada sobrecarga. Pero las pruebas unitarias no detectan problemas de interacción entre
componentes, que es el propósito de las pruebas de integración.
Si está escribiendo filtros personalizados, rutas, etc., debería realizar pruebas unitarias en ellos, pero no como parte
de las comprobaciones de una acción de controlador determinada, sino de forma aislada.
TIP
Cree y ejecute pruebas unitarias con Visual Studio.

Para explicar las pruebas unitarias, revisaremos el siguiente controlador. Muestra una lista de sesiones de lluvia de
ideas y permite crear nuevas sesiones de lluvia de ideas con un método POST:
using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using TestingControllersSample.Core.Interfaces;
using TestingControllersSample.Core.Model;
using TestingControllersSample.ViewModels;

namespace TestingControllersSample.Controllers
{
public class HomeController : Controller
{
private readonly IBrainstormSessionRepository _sessionRepository;

public HomeController(IBrainstormSessionRepository sessionRepository)


{
_sessionRepository = sessionRepository;
}

public async Task<IActionResult> Index()


{
var sessionList = await _sessionRepository.ListAsync();

var model = sessionList.Select(session => new StormSessionViewModel()


{
Id = session.Id,
DateCreated = session.DateCreated,
Name = session.Name,
IdeaCount = session.Ideas.Count
});

return View(model);
}

public class NewSessionModel


{
[Required]
public string SessionName { get; set; }
}

[HttpPost]
public async Task<IActionResult> Index(NewSessionModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
else
{
await _sessionRepository.AddAsync(new BrainstormSession()
{
DateCreated = DateTimeOffset.Now,
Name = model.SessionName
});
}

return RedirectToAction(actionName: nameof(Index));


}
}
}

El controlador sigue el principio de dependencias explícitas, de modo que espera que la inserción de dependencias
le proporcione una instancia de IBrainstormSessionRepository . Esto es bastante sencillo de comprobar si se usa un
marco de objeto ficticio, como Moq. El método HTTP GET Index no tiene bucles ni bifurcaciones y solamente llama
a un método. Para probar este método Index , tenemos que confirmar que se devuelve un ViewResult , con un
ViewModel del método List del repositorio.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Moq;
using TestingControllersSample.Controllers;
using TestingControllersSample.Core.Interfaces;
using TestingControllersSample.Core.Model;
using TestingControllersSample.ViewModels;
using Xunit;

namespace TestingControllersSample.Tests.UnitTests
{
public class HomeControllerTests
{
[Fact]
public async Task Index_ReturnsAViewResult_WithAListOfBrainstormSessions()
{
// Arrange
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.ListAsync()).Returns(Task.FromResult(GetTestSessions()));
var controller = new HomeController(mockRepo.Object);

// Act
var result = await controller.Index();

// Assert
var viewResult = Assert.IsType<ViewResult>(result);
var model = Assert.IsAssignableFrom<IEnumerable<StormSessionViewModel>>(
viewResult.ViewData.Model);
Assert.Equal(2, model.Count());
}

private List<BrainstormSession> GetTestSessions()


{
var sessions = new List<BrainstormSession>();
sessions.Add(new BrainstormSession()
{
DateCreated = new DateTime(2016, 7, 2),
Id = 1,
Name = "Test One"
});
sessions.Add(new BrainstormSession()
{
DateCreated = new DateTime(2016, 7, 1),
Id = 2,
Name = "Test Two"
});
return sessions;
}
}
}

El método HomeController HTTP POST Index (mostrado arriba) debe comprobar lo siguiente:
El método de acción devuelve un ViewResult de solicitud incorrecta con los datos adecuados cuando
ModelState.IsValid es false .

Se llama al método Add en el repositorio y se devuelve un RedirectToActionResult con los argumentos


correctos cuando ModelState.IsValid es true.
El estado de modelo no válido se puede comprobar introduciendo errores con AddModelError , como se muestra en
la primera prueba de abajo.

[Fact]
public async Task IndexPost_ReturnsBadRequestResult_WhenModelStateIsInvalid()
{
// Arrange
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.ListAsync()).Returns(Task.FromResult(GetTestSessions()));
var controller = new HomeController(mockRepo.Object);
controller.ModelState.AddModelError("SessionName", "Required");
var newSession = new HomeController.NewSessionModel();

// Act
var result = await controller.Index(newSession);

// Assert
var badRequestResult = Assert.IsType<BadRequestObjectResult>(result);
Assert.IsType<SerializableError>(badRequestResult.Value);
}

[Fact]
public async Task IndexPost_ReturnsARedirectAndAddsSession_WhenModelStateIsValid()
{
// Arrange
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.AddAsync(It.IsAny<BrainstormSession>()))
.Returns(Task.CompletedTask)
.Verifiable();
var controller = new HomeController(mockRepo.Object);
var newSession = new HomeController.NewSessionModel()
{
SessionName = "Test Name"
};

// Act
var result = await controller.Index(newSession);

// Assert
var redirectToActionResult = Assert.IsType<RedirectToActionResult>(result);
Assert.Null(redirectToActionResult.ControllerName);
Assert.Equal("Index", redirectToActionResult.ActionName);
mockRepo.Verify();
}

La primera prueba confirma cuándo ModelState no es válido; se devuelve el mismo ViewResult que para una
solicitud GET . Cabe decir que la prueba no intenta pasar un modelo no válido. Eso no funcionaría de todas formas,
ya que el enlace de modelos no se está ejecutando (aunque una prueba de integración sí usaría el enlace de
modelos). En este caso concreto no estamos comprobando el enlace de modelos. Con estas pruebas unitarias
solamente estamos comprobando lo que el código del método de acción hace.
La segunda prueba comprueba si, cuando ModelState es válido, se agrega un nuevo BrainstormSession (a través
del repositorio) y el método devuelve un RedirectToActionResult con las propiedades que se esperan. Las llamadas
ficticias que no se efectúan se suelen omitir, aunque llamar a Verifiable al final de la llamada nos permite
confirmar esto en la prueba. Esto se logra con una llamada a mockRepo.Verify , que producirá un error en la prueba
si no se ha llamado al método esperado.
NOTE
La biblioteca Moq usada en este ejemplo nos permite mezclar fácilmente objetos ficticios comprobables (o "estrictos") con
objetos ficticios no comprobables (también denominados "flexibles" o stub). Obtenga más información sobre cómo
personalizar el comportamiento de objetos ficticios con Moq.

Otro controlador de la aplicación muestra información relacionada con una sesión de lluvia de ideas determinada.
Este controlador incluye lógica para tratar los valores de identificador no válidos:

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using TestingControllersSample.Core.Interfaces;
using TestingControllersSample.ViewModels;

namespace TestingControllersSample.Controllers
{
public class SessionController : Controller
{
private readonly IBrainstormSessionRepository _sessionRepository;

public SessionController(IBrainstormSessionRepository sessionRepository)


{
_sessionRepository = sessionRepository;
}

public async Task<IActionResult> Index(int? id)


{
if (!id.HasValue)
{
return RedirectToAction(actionName: nameof(Index), controllerName: "Home");
}

var session = await _sessionRepository.GetByIdAsync(id.Value);


if (session == null)
{
return Content("Session not found.");
}

var viewModel = new StormSessionViewModel()


{
DateCreated = session.DateCreated,
Name = session.Name,
Id = session.Id
};

return View(viewModel);
}
}
}

La acción de controlador tiene tres casos que comprobar, uno por cada instrucción return :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Moq;
using TestingControllersSample.Controllers;
using TestingControllersSample.Core.Interfaces;
using TestingControllersSample.Core.Model;
using TestingControllersSample.ViewModels;
using Xunit;
namespace TestingControllersSample.Tests.UnitTests
{
public class SessionControllerTests
{
[Fact]
public async Task IndexReturnsARedirectToIndexHomeWhenIdIsNull()
{
// Arrange
var controller = new SessionController(sessionRepository: null);

// Act
var result = await controller.Index(id: null);

// Assert
var redirectToActionResult = Assert.IsType<RedirectToActionResult>(result);
Assert.Equal("Home", redirectToActionResult.ControllerName);
Assert.Equal("Index", redirectToActionResult.ActionName);
}

[Fact]
public async Task IndexReturnsContentWithSessionNotFoundWhenSessionNotFound()
{
// Arrange
int testSessionId = 1;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.Returns(Task.FromResult((BrainstormSession)null));
var controller = new SessionController(mockRepo.Object);

// Act
var result = await controller.Index(testSessionId);

// Assert
var contentResult = Assert.IsType<ContentResult>(result);
Assert.Equal("Session not found.", contentResult.Content);
}

[Fact]
public async Task IndexReturnsViewResultWithStormSessionViewModel()
{
// Arrange
int testSessionId = 1;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.Returns(Task.FromResult(GetTestSessions().FirstOrDefault(s => s.Id == testSessionId)));
var controller = new SessionController(mockRepo.Object);

// Act
var result = await controller.Index(testSessionId);

// Assert
var viewResult = Assert.IsType<ViewResult>(result);
var model = Assert.IsType<StormSessionViewModel>(viewResult.ViewData.Model);
Assert.Equal("Test One", model.Name);
Assert.Equal(2, model.DateCreated.Day);
Assert.Equal(testSessionId, model.Id);
}

private List<BrainstormSession> GetTestSessions()


{
var sessions = new List<BrainstormSession>();
sessions.Add(new BrainstormSession()
{
DateCreated = new DateTime(2016, 7, 2),
Id = 1,
Name = "Test One"
});
sessions.Add(new BrainstormSession()
{
DateCreated = new DateTime(2016, 7, 1),
Id = 2,
Name = "Test Two"
});
return sessions;
}
}
}

La aplicación expone la funcionalidad como una API web (una lista de ideas asociadas a una sesión de lluvia de
ideas y un método para agregar nuevas ideas a una sesión):

using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using TestingControllersSample.ClientModels;
using TestingControllersSample.Core.Interfaces;
using TestingControllersSample.Core.Model;

namespace TestingControllersSample.Api
{
[Route("api/ideas")]
public class IdeasController : Controller
{
private readonly IBrainstormSessionRepository _sessionRepository;

public IdeasController(IBrainstormSessionRepository sessionRepository)


{
_sessionRepository = sessionRepository;
}

[HttpGet("forsession/{sessionId}")]
public async Task<IActionResult> ForSession(int sessionId)
{
var session = await _sessionRepository.GetByIdAsync(sessionId);
if (session == null)
{
return NotFound(sessionId);
}

var result = session.Ideas.Select(idea => new IdeaDTO()


{
Id = idea.Id,
Name = idea.Name,
Description = idea.Description,
DateCreated = idea.DateCreated
}).ToList();

return Ok(result);
}

[HttpPost("create")]
public async Task<IActionResult> Create([FromBody]NewIdeaModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}

var session = await _sessionRepository.GetByIdAsync(model.SessionId);


if (session == null)
{
return NotFound(model.SessionId);
}
var idea = new Idea()
{
DateCreated = DateTimeOffset.Now,
Description = model.Description,
Name = model.Name
};
session.AddIdea(idea);

await _sessionRepository.UpdateAsync(session);

return Ok(session);
}
}
}

El método ForSession devuelve una lista de tipos de IdeaDTO . Evite devolver entidades de dominio de empresa
directamente a través de llamadas API, ya que con frecuencia incluyen más datos de los que el cliente de API
requiere y asocian innecesariamente el modelo de dominio interno de su aplicación con la API que se expone
externamente. La asignación entre las entidades de dominio y los tipos que se van a devolver se puede realizar
manualmente (usando un método Select de LINQ, tal y como se muestra aquí) o por medio de una biblioteca
como AutoMapper.
Estas son las pruebas unitarias de los métodos API Create y ForSession :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Moq;
using TestingControllersSample.Api;
using TestingControllersSample.ClientModels;
using TestingControllersSample.Core.Interfaces;
using TestingControllersSample.Core.Model;
using Xunit;

namespace TestingControllersSample.Tests.UnitTests
{
public class ApiIdeasControllerTests
{
[Fact]
public async Task Create_ReturnsBadRequest_GivenInvalidModel()
{
// Arrange & Act
var mockRepo = new Mock<IBrainstormSessionRepository>();
var controller = new IdeasController(mockRepo.Object);
controller.ModelState.AddModelError("error","some error");

// Act
var result = await controller.Create(model: null);

// Assert
Assert.IsType<BadRequestObjectResult>(result);
}

[Fact]
public async Task Create_ReturnsHttpNotFound_ForInvalidSession()
{
// Arrange
int testSessionId = 123;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.Returns(Task.FromResult((BrainstormSession)null));
var controller = new IdeasController(mockRepo.Object);
// Act
var result = await controller.Create(new NewIdeaModel());

// Assert
Assert.IsType<NotFoundObjectResult>(result);
}

[Fact]
public async Task Create_ReturnsNewlyCreatedIdeaForSession()
{
// Arrange
int testSessionId = 123;
string testName = "test name";
string testDescription = "test description";
var testSession = GetTestSession();
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.Returns(Task.FromResult(testSession));
var controller = new IdeasController(mockRepo.Object);

var newIdea = new NewIdeaModel()


{
Description = testDescription,
Name = testName,
SessionId = testSessionId
};
mockRepo.Setup(repo => repo.UpdateAsync(testSession))
.Returns(Task.CompletedTask)
.Verifiable();

// Act
var result = await controller.Create(newIdea);

// Assert
var okResult = Assert.IsType<OkObjectResult>(result);
var returnSession = Assert.IsType<BrainstormSession>(okResult.Value);
mockRepo.Verify();
Assert.Equal(2, returnSession.Ideas.Count());
Assert.Equal(testName, returnSession.Ideas.LastOrDefault().Name);
Assert.Equal(testDescription, returnSession.Ideas.LastOrDefault().Description);
}

private BrainstormSession GetTestSession()


{
var session = new BrainstormSession()
{
DateCreated = new DateTime(2016, 7, 2),
Id = 1,
Name = "Test One"
};

var idea = new Idea() { Name = "One" };


session.AddIdea(idea);
return session;
}
}
}

Como se ha indicado anteriormente, si quiere comprobar el comportamiento del método cuando ModelState no es
válido, agregue un error de modelo al controlador como parte de la prueba. No intente probar la validación del
modelo o el enlace de modelos en las pruebas unitarias: céntrese tan solo en el comportamiento de su método de
acción al confrontarlo con un valor de ModelState determinado.
La segunda prueba depende de que el repositorio devuelva null, por lo que el repositorio ficticio está configurado
para devolver un valor null. No es necesario crear una base de datos de prueba (en memoria o de cualquier otro
modo) ni crear una consulta que devuelva este resultado. Esto se puede realizar en una sola instrucción, tal y como
se muestra.
La última prueba confirma que se llama al método Update del repositorio. Tal y como hicimos anteriormente, se
llama al objeto ficticio con Verifiable y, después, se llama al método Verify del repositorio ficticio para confirmar
que el método Verifiable se ha ejecutado. Las pruebas unitarias no se encargan de garantizar que el método
Update guarda los datos; esto se puede realizar con una prueba de integración.

Pruebas de integración
Las pruebas de integración se realizan para garantizar que distintos módulos independientes dentro de la
aplicación funcionan correctamente juntos. Por lo general, todo lo que se puede comprobar con una prueba unitaria
también se puede comprobar con una prueba de integración, pero no a la inversa. Pero las pruebas de integración
suelen ser mucho más lentas que las unitarias. Por tanto, lo mejor es comprobar todo lo que sea factible con las
pruebas unitarias y recurrir a las pruebas de integración en los casos en los que existan varios colaboradores.
Los objetos ficticios rara vez se usan en las pruebas de integración, aunque pueden seguir siendo de utilidad. En las
pruebas unitarias, los objetos ficticios constituyen un método eficaz de controlar el modo en que los colaboradores
fuera de la unidad que se está probando deben comportarse según los propósitos de la prueba. En una prueba de
integración se usan colaboradores reales para confirmar que todo el subsistema funciona correctamente en
conjunto.
Estado de la aplicación
Una consideración importante al realizar pruebas de integración es cómo establecer el estado de la aplicación. Las
pruebas se deben ejecutar de manera independiente entre sí, por lo que cada prueba debe comenzar con la
aplicación en un estado conocido. Que la aplicación no use una base de datos o tenga algún tipo de persistencia no
debe ser un problema. Pero la mayoría de las aplicaciones reales almacenan su estado en algún tipo de almacén de
datos, de modo que cualquier modificación que se realice a raíz de una prueba puede repercutir en otra, a menos
que se restablezca el almacén de datos. Si se usa el método integrado TestServer , será muy fácil hospedar
aplicaciones ASP.NET Core en nuestras pruebas de integración, pero esto no da acceso necesariamente a los datos
que se van a usar. Si se usa una base de datos real, un método consiste en conectar la aplicación a una base de
datos de prueba, a la que las pruebas pueden obtener acceso, y confirmar que está restablecida en un estado
conocido antes de que cada prueba se ejecute.
En esta aplicación de ejemplo, uso la base de datos InMemoryDatabase de Entity Framework Core, por lo que no
puedo simplemente conectarme a ella desde mi proyecto de prueba. En su lugar, expondré un método
InitializeDatabase desde la clase Startup de la aplicación, y llamaré a ese método cuando la aplicación se inicie si
está en el entorno Development . Mis pruebas de integración sacarán partido de esto automáticamente siempre que
tengan el entorno establecido en Development . No tiene que preocuparse de restablecer la base de datos, ya que
InMemoryDatabase se restablece cada vez que la aplicación se reinicia.
La clase Startup :

using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using TestingControllersSample.Core.Interfaces;
using TestingControllersSample.Core.Model;
using TestingControllersSample.Infrastructure;

namespace TestingControllersSample
{
public class Startup
{
{
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<AppDbContext>(
optionsBuilder => optionsBuilder.UseInMemoryDatabase("InMemoryDb"));

services.AddMvc();

services.AddScoped<IBrainstormSessionRepository,
EFStormSessionRepository>();
}

public void Configure(IApplicationBuilder app,


IHostingEnvironment env,
ILoggerFactory loggerFactory)
{
if (env.IsDevelopment())
{
var repository = app.ApplicationServices.GetService<IBrainstormSessionRepository>();
InitializeDatabaseAsync(repository).Wait();
}

app.UseStaticFiles();

app.UseMvcWithDefaultRoute();
}

public async Task InitializeDatabaseAsync(IBrainstormSessionRepository repo)


{
var sessionList = await repo.ListAsync();
if (!sessionList.Any())
{
await repo.AddAsync(GetTestSession());
}
}

public static BrainstormSession GetTestSession()


{
var session = new BrainstormSession()
{
Name = "Test Session 1",
DateCreated = new DateTime(2016, 8, 1)
};
var idea = new Idea()
{
DateCreated = new DateTime(2016, 8, 1),
Description = "Totally awesome idea",
Name = "Awesome idea"
};
session.AddIdea(idea);
return session;
}
}
}

Verá que el método GetTestSession se usa con bastante asiduidad en las siguientes pruebas de integración.
Acceso a las vistas
En cada clase de prueba de integración se configura el método TestServer que ejecutará la aplicación de ASP.NET
Core. TestServer hospeda la aplicación web de forma predeterminada en la carpeta donde se está ejecutando (en
este caso, la carpeta del proyecto de prueba). Por tanto, si intenta probar las acciones del controlador que devuelven
ViewResult , es posible que aparezca este error:
The view 'Index' wasn't found. The following locations were searched:
(list of locations)

Para corregir este problema, debe configurar la raíz del contenido del servidor de forma que pueda localizar las
vistas del proyecto que se está comprobando. Esto se consigue con una llamada a UseContentRoot en la clase
TestFixture , como se aprecia aquí:

using System;
using System.IO;
using System.Net.Http;
using System.Reflection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.ViewComponents;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;

namespace TestingControllersSample.Tests.IntegrationTests
{
/// <summary>
/// A test fixture which hosts the target project (project we wish to test) in an in-memory server.
/// </summary>
/// <typeparam name="TStartup">Target project's startup type</typeparam>
public class TestFixture<TStartup> : IDisposable
{
private readonly TestServer _server;

public TestFixture()
: this(Path.Combine("src"))
{
}

protected TestFixture(string relativeTargetProjectParentDir)


{
var startupAssembly = typeof(TStartup).GetTypeInfo().Assembly;
var contentRoot = GetProjectPath(relativeTargetProjectParentDir, startupAssembly);

var builder = new WebHostBuilder()


.UseContentRoot(contentRoot)
.ConfigureServices(InitializeServices)
.UseEnvironment("Development")
.UseStartup(typeof(TStartup));

_server = new TestServer(builder);

Client = _server.CreateClient();
Client.BaseAddress = new Uri("http://localhost");
}

public HttpClient Client { get; }

public void Dispose()


{
Client.Dispose();
_server.Dispose();
}

protected virtual void InitializeServices(IServiceCollection services)


{
var startupAssembly = typeof(TStartup).GetTypeInfo().Assembly;

// Inject a custom application part manager.


// Overrides AddMvcCore() because it uses TryAdd().
var manager = new ApplicationPartManager();
manager.ApplicationParts.Add(new AssemblyPart(startupAssembly));
manager.ApplicationParts.Add(new AssemblyPart(startupAssembly));
manager.FeatureProviders.Add(new ControllerFeatureProvider());
manager.FeatureProviders.Add(new ViewComponentFeatureProvider());

services.AddSingleton(manager);
}

/// <summary>
/// Gets the full path to the target project that we wish to test
/// </summary>
/// <param name="projectRelativePath">
/// The parent directory of the target project.
/// e.g. src, samples, test, or test/Websites
/// </param>
/// <param name="startupAssembly">The target project's assembly.</param>
/// <returns>The full path to the target project.</returns>
private static string GetProjectPath(string projectRelativePath, Assembly startupAssembly)
{
// Get name of the target project which we want to test
var projectName = startupAssembly.GetName().Name;

// Get currently executing test project path


var applicationBasePath = System.AppContext.BaseDirectory;

// Find the path to the target project


var directoryInfo = new DirectoryInfo(applicationBasePath);
do
{
directoryInfo = directoryInfo.Parent;

var projectDirectoryInfo = new DirectoryInfo(Path.Combine(directoryInfo.FullName,


projectRelativePath));
if (projectDirectoryInfo.Exists)
{
var projectFileInfo = new FileInfo(Path.Combine(projectDirectoryInfo.FullName, projectName,
$"{projectName}.csproj"));
if (projectFileInfo.Exists)
{
return Path.Combine(projectDirectoryInfo.FullName, projectName);
}
}
}
while (directoryInfo.Parent != null);

throw new Exception($"Project root could not be located using the application root
{applicationBasePath}.");
}
}
}

La clase TestFixture se encarga de configurar y crear el método TestServer , que configura un HttpClient para
comunicarse con dicho método TestServer . En cada una de las pruebas de integración se usa la propiedad Client
para conectarse al servidor de prueba y realizar una solicitud.
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Xunit;

namespace TestingControllersSample.Tests.IntegrationTests
{
public class HomeControllerTests : IClassFixture<TestFixture<TestingControllersSample.Startup>>
{
private readonly HttpClient _client;

public HomeControllerTests(TestFixture<TestingControllersSample.Startup> fixture)


{
_client = fixture.Client;
}

[Fact]
public async Task ReturnsInitialListOfBrainstormSessions()
{
// Arrange - get a session known to exist
var testSession = Startup.GetTestSession();

// Act
var response = await _client.GetAsync("/");

// Assert
response.EnsureSuccessStatusCode();
var responseString = await response.Content.ReadAsStringAsync();
Assert.Contains(testSession.Name, responseString);
}

[Fact]
public async Task PostAddsNewBrainstormSession()
{
// Arrange
string testSessionName = Guid.NewGuid().ToString();
var data = new Dictionary<string, string>();
data.Add("SessionName", testSessionName);
var content = new FormUrlEncodedContent(data);

// Act
var response = await _client.PostAsync("/", content);

// Assert
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.Equal("/", response.Headers.Location.ToString());
}
}
}

En la primera prueba de arriba, responseString contiene el HTML realmente presentado de la vista, que se puede
revisar para confirmar que contiene los resultados esperados.
La segunda prueba crea un formulario POST con un nombre de sesión único y lo envía a la aplicación para,
seguidamente, confirmar que se devuelve la redirección prevista.
Métodos de API
Si la aplicación expone API web, conviene confirmar que se ejecutan según lo previsto por medio de pruebas
automatizadas. El método integrado TestServer permite comprobar API web de forma muy sencilla. Si los
métodos de API usan el enlace de modelos, deberá comprobar siempre el factor ModelState.IsValid y, en este
sentido, las pruebas de integración son el lugar adecuado para confirmar que la validación del modelo funciona
correctamente.
El siguiente conjunto de pruebas tiene como destino el método Create en la clase IdeasController de arriba:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Newtonsoft.Json;
using TestingControllersSample.ClientModels;
using TestingControllersSample.Core.Model;
using Xunit;

namespace TestingControllersSample.Tests.IntegrationTests
{
public class ApiIdeasControllerTests : IClassFixture<TestFixture<TestingControllersSample.Startup>>
{
internal class NewIdeaDto
{
public NewIdeaDto(string name, string description, int sessionId)
{
Name = name;
Description = description;
SessionId = sessionId;
}

public string Name { get; set; }


public string Description { get; set; }
public int SessionId { get; set; }
}

private readonly HttpClient _client;

public ApiIdeasControllerTests(TestFixture<TestingControllersSample.Startup> fixture)


{
_client = fixture.Client;
}

[Fact]
public async Task CreatePostReturnsBadRequestForMissingNameValue()
{
// Arrange
var newIdea = new NewIdeaDto("", "Description", 1);

// Act
var response = await _client.PostAsJsonAsync("/api/ideas/create", newIdea);

// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}

[Fact]
public async Task CreatePostReturnsBadRequestForMissingDescriptionValue()
{
// Arrange
var newIdea = new NewIdeaDto("Name", "", 1);

// Act
var response = await _client.PostAsJsonAsync("/api/ideas/create", newIdea);

// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}

[Fact]
public async Task CreatePostReturnsBadRequestForSessionIdValueTooSmall()
{
// Arrange
// Arrange
var newIdea = new NewIdeaDto("Name", "Description", 0);

// Act
var response = await _client.PostAsJsonAsync("/api/ideas/create", newIdea);

// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}

[Fact]
public async Task CreatePostReturnsBadRequestForSessionIdValueTooLarge()
{
// Arrange
var newIdea = new NewIdeaDto("Name", "Description", 1000001);

// Act
var response = await _client.PostAsJsonAsync("/api/ideas/create", newIdea);

// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}

[Fact]
public async Task CreatePostReturnsNotFoundForInvalidSession()
{
// Arrange
var newIdea = new NewIdeaDto("Name", "Description", 123);

// Act
var response = await _client.PostAsJsonAsync("/api/ideas/create", newIdea);

// Assert
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}

[Fact]
public async Task CreatePostReturnsCreatedIdeaWithCorrectInputs()
{
// Arrange
var testIdeaName = Guid.NewGuid().ToString();
var newIdea = new NewIdeaDto(testIdeaName, "Description", 1);

// Act
var response = await _client.PostAsJsonAsync("/api/ideas/create", newIdea);

// Assert
response.EnsureSuccessStatusCode();
var returnedSession = await response.Content.ReadAsJsonAsync<BrainstormSession>();
Assert.Equal(2, returnedSession.Ideas.Count);
Assert.Contains(testIdeaName, returnedSession.Ideas.Select(i => i.Name).ToList());
}

[Fact]
public async Task ForSessionReturnsNotFoundForBadSessionId()
{
// Arrange & Act
var response = await _client.GetAsync("/api/ideas/forsession/500");

// Assert
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}

[Fact]
public async Task ForSessionReturnsIdeasForValidSessionId()
{
// Arrange
var testSession = Startup.GetTestSession();

// Act
// Act
var response = await _client.GetAsync("/api/ideas/forsession/1");

// Assert
response.EnsureSuccessStatusCode();
var ideaList = JsonConvert.DeserializeObject<List<IdeaDTO>>(
await response.Content.ReadAsStringAsync());
var firstIdea = ideaList.First();
Assert.Equal(testSession.Ideas.First().Name, firstIdea.Name);
}
}
}

A diferencia de las pruebas de integración de acciones que devuelven vistas HTML, los métodos de API web que
devuelven resultados se suelen poder deserializar como objetos fuertemente tipados, tal y como arroja la última
prueba mostrada arriba. En este caso, la prueba deserializa el resultado en una instancia de BrainstormSession y
confirma que la idea se agregó correctamente a la colección de ideas.
En el proyecto de ejemplo de este artículo encontrará más ejemplos de pruebas de integración.
Temas avanzados de ASP.NET Core MVC
21/06/2018 • 2 minutes to read • Edit Online

Trabajar con el modelo de aplicación


Filtros
Áreas
Elementos de la aplicación
Enlace de modelos personalizado
Trabajar con el modelo de aplicación en ASP.NET
Core
25/06/2018 • 18 minutes to read • Edit Online

Por Steve Smith


ASP.NET Core MVC define un modelo de aplicación que representa los componentes de una aplicación MVC.
Puede leer y manipular este modelo para modificar la manera en que se comportan los elementos de MVC. De
forma predeterminada, MVC sigue ciertas convenciones para determinar qué clases se consideran controladores,
qué métodos de esas clases son acciones y cómo se comportan los parámetros y el enrutamiento. Puede
personalizar este comportamiento para adaptarlo a las necesidades de su aplicación. Para ello, basta con que cree
sus propias convenciones y las aplique globalmente o como atributos.

Modelos y proveedores
El modelo de aplicación de ASP.NET Core MVC incluye interfaces abstractas y clases de implementación concretas
que describen una aplicación MVC. Este modelo es el resultado de la detección por parte de MVC de los
controladores, las acciones, los parámetros de acción, las rutas y los filtros de la aplicación de acuerdo con las
convenciones predeterminadas. Cuando trabaje con el modelo de aplicación, puede modificar la aplicación para
que siga convenciones diferentes del comportamiento predeterminado de MVC. Los parámetros, nombres, rutas y
filtros se usan como datos de configuración para las acciones y los controladores.
El modelo de aplicación de ASP.NET Core MVC tiene la estructura siguiente:
ApplicationModel
Controladores (ControllerModel)
Acciones (ActionModel)
Parámetros (ParameterModel)
Cada nivel del modelo tiene acceso a una colección Properties común, y los niveles inferiores pueden tener
acceso a los valores de propiedad establecidos por los niveles superiores de la jerarquía y sobrescribirlos. Las
propiedades se conservan en ActionDescriptor.Properties cuando se crean las acciones. Después, cuando se
controla una solicitud, se puede obtener acceso a través de ActionContext.ActionDescriptor.Properties a todas las
propiedades que agregue o modifique una convención. El uso de propiedades es una manera excelente de
configurar por acción los filtros, los enlazadores de modelos, etc.

NOTE
La colección ActionDescriptor.Properties no es segura para subprocesos (para escrituras) una vez que el inicio de la
aplicación haya finalizado. Las convenciones son la mejor manera de agregar datos de forma segura a esta colección.

IApplicationModelProvider
ASP.NET Core MVC carga el modelo de aplicación mediante un patrón de proveedor definido por la interfaz
IApplicationModelProvider. En esta sección se describen algunos detalles de implementación interna relacionados
con el funcionamiento de este proveedor. Se trata de un tema avanzado, ya que la mayoría de las aplicaciones que
aprovechan el modelo de aplicación deberían hacerlo mediante convenciones.
Las implementaciones de la interfaz IApplicationModelProvider "se encapsulan" entre sí, y cada implementación
llama a OnProvidersExecuting en orden ascendente en función de su propiedad Order . Después, se llama al
método OnProvidersExecuted en orden inverso. El marco de trabajo define varios proveedores:
Primero, ( Order=-1000 ):
DefaultApplicationModelProvider

Después, ( Order=-990 ):
AuthorizationApplicationModelProvider
CorsApplicationModelProvider

NOTE
El orden en que se llama a dos proveedores con el mismo valor para Order no está definido y, por tanto, no se debe confiar
en él.

NOTE
IApplicationModelProvider es un concepto avanzado pensado para que los autores del marco de trabajo lo extiendan. En
general, las aplicaciones deben usar convenciones y los marcos de trabajo deben usar proveedores. La diferencia clave es que
los proveedores siempre se ejecutan antes que las convenciones.

DefaultApplicationModelProvider establece muchos de los comportamientos predeterminados que usa ASP.NET


Core MVC. Entre sus responsabilidades se incluyen las siguientes:
Agregar filtros globales al contexto
Agregar controladores al contexto
Agregar métodos de controlador públicos como acciones
Agregar parámetros de métodos de acción al contexto
Aplicar la ruta y otros atributos
Algunos comportamientos integrados se implementan mediante DefaultApplicationModelProvider . Este proveedor
es responsable de la construcción de ControllerModel , que a su vez hace referencia a instancias de ActionModel ,
PropertyModel y ParameterModel . La clase DefaultApplicationModelProvider es un detalle de implementación del
marco de trabajo interno que cambiará en el futuro.
AuthorizationApplicationModelProvider se encarga de aplicar el comportamiento asociado a los atributos
AuthorizeFilter y AllowAnonymousFilter . Más información sobre estos atributos.
CorsApplicationModelProvider implementa el comportamiento asociado a IEnableCorsAttribute ,
IDisableCorsAttribute y DisableCorsAuthorizationFilter . Más información sobre CORS.

Convenciones
El modelo de aplicación define abstracciones de convenciones que proporcionan una forma más sencilla de
personalizar el comportamiento de los modelos que invalidan el modelo completo o el proveedor. Se recomienda
el uso de estas abstracciones para modificar el comportamiento de la aplicación. Las convenciones ofrecen una
manera de escribir código que aplica dinámicamente las personalizaciones. Mientras los filtros proporcionan una
forma de modificar el comportamiento del marco de trabajo, las personalizaciones permiten controlar cómo se
conecta la aplicación en su conjunto.
Están disponibles las convenciones siguientes:
IApplicationModelConvention
IControllerModelConvention
IActionModelConvention
IParameterModelConvention

Para aplicar las convenciones, se agregan a las opciones de MVC, o bien se implementan valores de tipo
Attribute y se aplican a controladores, acciones o parámetros de acción (de forma similar al uso de Filters ). A
diferencia de los filtros, las convenciones solo se ejecutan cuando se inicia la aplicación, no como parte de cada
solicitud.
Ejemplo: modificar ApplicationModel
La siguiente convención se usa para agregar una propiedad al modelo de aplicación.

using Microsoft.AspNetCore.Mvc.ApplicationModels;

namespace AppModelSample.Conventions
{
public class ApplicationDescription : IApplicationModelConvention
{
private readonly string _description;

public ApplicationDescription(string description)


{
_description = description;
}

public void Apply(ApplicationModel application)


{
application.Properties["description"] = _description;
}
}
}

Las convenciones del modelo de aplicación se aplican como opciones cuando se agrega MVC en
ConfigureServices en Startup .

public void ConfigureServices(IServiceCollection services)


{
services.AddMvc(options =>
{
options.Conventions.Add(new ApplicationDescription("My Application Description"));
options.Conventions.Add(new NamespaceRoutingConvention());
//options.Conventions.Add(new IdsMustBeInRouteParameterModelConvention());
});
}

Las propiedades son accesibles desde la colección de propiedades ActionDescriptor dentro de las acciones de
controlador:

public class AppModelController : Controller


{
public string Description()
{
return "Description: " + ControllerContext.ActionDescriptor.Properties["description"];
}
}

Ejemplo: modificar la descripción de ControllerModel


Como en el ejemplo anterior, el modelo de controlador también se puede modificar para incluir propiedades
personalizadas. Estos invalidará las propiedades existentes con el mismo nombre especificado en el modelo de
aplicación. El atributo de convención siguiente agrega una descripción en el nivel de controlador:

using System;
using Microsoft.AspNetCore.Mvc.ApplicationModels;

namespace AppModelSample.Conventions
{
public class ControllerDescriptionAttribute : Attribute, IControllerModelConvention
{
private readonly string _description;

public ControllerDescriptionAttribute(string description)


{
_description = description;
}

public void Apply(ControllerModel controllerModel)


{
controllerModel.Properties["description"] = _description;
}
}
}

Esta convención se aplica como un atributo en un controlador.

[ControllerDescription("Controller Description")]
public class DescriptionAttributesController : Controller
{
public string Index()
{
return "Description: " + ControllerContext.ActionDescriptor.Properties["description"];
}

Se tiene acceso a la propiedad "description" de la misma manera que en ejemplos anteriores.


Ejemplo: modificar la descripción de ActionModel
Se puede aplicar una convención de atributo independiente a acciones individuales, con lo que se invalida el
comportamiento que ya se haya aplicado en el nivel de aplicación o controlador.

using System;
using Microsoft.AspNetCore.Mvc.ApplicationModels;

namespace AppModelSample.Conventions
{
public class ActionDescriptionAttribute : Attribute, IActionModelConvention
{
private readonly string _description;

public ActionDescriptionAttribute(string description)


{
_description = description;
}

public void Apply(ActionModel actionModel)


{
actionModel.Properties["description"] = _description;
}
}
}

Si se aplica a una acción dentro del controlador del ejemplo anterior se puede ver cómo invalida la convención en
el nivel de controlador:

[ControllerDescription("Controller Description")]
public class DescriptionAttributesController : Controller
{
public string Index()
{
return "Description: " + ControllerContext.ActionDescriptor.Properties["description"];
}

[ActionDescription("Action Description")]
public string UseActionDescriptionAttribute()
{
return "Description: " + ControllerContext.ActionDescriptor.Properties["description"];
}
}

Ejemplo: modificar ParameterModel


La convención siguiente se puede aplicar a parámetros de acción para modificar su BindingInfo . La convención
siguiente requiere que el parámetro sea un parámetro de ruta. Se ignorarán todos los demás orígenes de enlace
posibles (por ejemplo, valores de cadena de consulta).

using System;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.ModelBinding;

namespace AppModelSample.Conventions
{
public class MustBeInRouteParameterModelConvention : Attribute, IParameterModelConvention
{
public void Apply(ParameterModel model)
{
if (model.BindingInfo == null)
{
model.BindingInfo = new BindingInfo();
}
model.BindingInfo.BindingSource = BindingSource.Path;
}
}
}

El atributo se puede aplicar a cualquier parámetro de acción:

public class ParameterModelController : Controller


{
// Will bind: /ParameterModel/GetById/123
// WON'T bind: /ParameterModel/GetById?id=123
public string GetById([MustBeInRouteParameterModelConvention]int id)
{
return $"Bound to id: {id}";
}
}

Ejemplo: modificar el nombre de ActionModel


La convención siguiente modifica el valor de ActionModel para actualizar el nombre de la acción a la que se aplica.
El nuevo nombre se proporciona como un parámetro al atributo. El enrutamiento usará este nuevo nombre, lo que
afectará a la ruta usada para llegar a este método de acción.
using System;
using Microsoft.AspNetCore.Mvc.ApplicationModels;

namespace AppModelSample.Conventions
{
public class CustomActionNameAttribute : Attribute, IActionModelConvention
{
private readonly string _actionName;

public CustomActionNameAttribute(string actionName)


{
_actionName = actionName;
}

public void Apply(ActionModel actionModel)


{
// this name will be used by routing
actionModel.ActionName = _actionName;
}
}
}

Este atributo se aplica a un método de acción en HomeController :

// Route: /Home/MyCoolAction
[CustomActionName("MyCoolAction")]
public string SomeName()
{
return ControllerContext.ActionDescriptor.ActionName;
}

Aunque el nombre del método es SomeName , el atributo invalida la convención de MVC de usar el nombre del
método y reemplaza el nombre de la acción por MyCoolAction . De este modo, la ruta usada para llegar a esta
acción es /Home/MyCoolAction .

NOTE
Este ejemplo básicamente equivale a usar el atributo ActionName integrado.

Ejemplo: convención de enrutamiento personalizado


Puede usar IApplicationModelConvention para personalizar cómo funciona el enrutamiento. Por ejemplo, la
convención siguiente incorporará espacios de nombres de controladores en sus rutas, al reemplazar . en el
espacio de nombres por / en la ruta:
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using System.Linq;

namespace AppModelSample.Conventions
{
public class NamespaceRoutingConvention : IApplicationModelConvention
{
public void Apply(ApplicationModel application)
{
foreach (var controller in application.Controllers)
{
var hasAttributeRouteModels = controller.Selectors
.Any(selector => selector.AttributeRouteModel != null);

if (!hasAttributeRouteModels
&& controller.ControllerName.Contains("Namespace")) // affect one controller in this
sample
{
// Replace the . in the namespace with a / to create the attribute route
// Ex: MySite.Admin namespace will correspond to MySite/Admin attribute route
// Then attach [controller], [action] and optional {id?} token.
// [Controller] and [action] is replaced with the controller and action
// name to generate the final template
controller.Selectors[0].AttributeRouteModel = new AttributeRouteModel()
{
Template = controller.ControllerType.Namespace.Replace('.', '/') +
"/[controller]/[action]/{id?}"
};
}
}

// You can continue to put attribute route templates for the controller actions depending on the
way you want them to behave
}
}
}

La convención se agrega como una opción en Startup.

public void ConfigureServices(IServiceCollection services)


{
services.AddMvc(options =>
{
options.Conventions.Add(new ApplicationDescription("My Application Description"));
options.Conventions.Add(new NamespaceRoutingConvention());
//options.Conventions.Add(new IdsMustBeInRouteParameterModelConvention());
});
}

TIP
Para agregar convenciones a su software intermedio, obtenga acceso a MvcOptions con
services.Configure<MvcOptions>(c => c.Conventions.Add(YOURCONVENTION)); .

En este ejemplo se aplica esta convención a las rutas que no usan el enrutamiento de atributos donde el
controlador contiene "Namespace" en su nombre. En el controlador siguiente se muestra esta convención:
using Microsoft.AspNetCore.Mvc;

namespace AppModelSample.Controllers
{
public class NamespaceRoutingController : Controller
{
// using NamespaceRoutingConvention
// route: /AppModelSample/Controllers/NamespaceRouting/Index
public string Index()
{
return "This demonstrates namespace routing.";
}
}
}

Uso del modelo de aplicación en WebApiCompatShim


ASP.NET Core MVC usa un conjunto diferente de convenciones de ASP.NET Web API 2. Mediante el uso de
convenciones personalizadas, puede modificar el comportamiento de una aplicación ASP.NET Core MVC para que
sea coherente con el de una aplicación Web API. Microsoft suministra WebApiCompatShim específicamente para
este propósito.

NOTE
Obtenga más información sobre la migración desde ASP.NET Web API.

Para usar las correcciones de compatibilidad (shim) de Web API, debe agregar el paquete al proyecto y, después,
agregar las convenciones a MVC mediante una llamada a AddWebApiConventions en Startup :

services.AddMvc().AddWebApiConventions();

Las convenciones proporcionadas por las correcciones de compatibilidad solo se aplican a las partes de la
aplicación a las que se han aplicado ciertos atributos. Los cuatro atributos siguientes se usan para controlar en qué
controladores se deben modificar las convenciones mediante las convenciones de las correcciones de
compatibilidad:
UseWebApiActionConventionsAttribute
UseWebApiOverloadingAttribute
UseWebApiParameterConventionsAttribute
UseWebApiRoutesAttribute
Convenciones de acción
UseWebApiActionConventionsAttribute se usa para asignar el método HTTP a las acciones según su nombre (por
ejemplo, Get )
se asignaría a HttpGet . Solo se aplica a las acciones que no usan el enrutamiento de atributos.
Sobrecarga
UseWebApiOverloadingAttribute se usa para aplicar la convención WebApiOverloadingApplicationModelConvention .
Esta convención agrega OverloadActionConstraint al proceso de selección de acciones, que limita las acciones
candidatas a aquellas en las que la solicitud cumple todos los parámetros no opcionales.
Convenciones de parámetro
UseWebApiParameterConventionsAttribute se usa para aplicar la convención de acción
WebApiParameterConventionsApplicationModelConvention . Esta convención especifica que los tipos simples usados
como parámetros de acción se enlazan desde el URI de forma predeterminada, mientras que los tipos complejos
se enlazan desde el cuerpo de la solicitud.
Rutas
UseWebApiRoutesAttribute controla si se ha aplicado la convención de controlador
WebApiApplicationModelConvention . Cuando se habilita, esta convención se usa para agregar a la ruta
compatibilidad con áreas.
Además de un conjunto de convenciones, el paquete de compatibilidad incluye una clase base
System.Web.Http.ApiController que reemplaza la que proporciona Web API. Esto permite que los controladores
escritos para Web API y que heredan de ApiController funcionen de conformidad con su diseño, mientras se
ejecutan en ASP.NET Core MVC. Esta clase de controlador base se decora con todos los atributos UseWebApi*
mencionados anteriormente. ApiController expone propiedades, métodos y tipos de resultados compatibles con
los que se encuentran en Web API.

Uso de ApiExplorer para documentar la aplicación


El modelo de aplicación expone una propiedad ApiExplorer en cada nivel que se puede usar para recorrer la
estructura de la aplicación. Esto se puede usar para generar páginas de ayuda para las Web API mediante el uso de
herramientas como Swagger. La propiedad ApiExplorer expone una propiedad IsVisible que se puede
establecer para especificar qué partes del modelo de la aplicación deben exponerse. Puede configurar esta opción
mediante una convención:

using Microsoft.AspNetCore.Mvc.ApplicationModels;

namespace AppModelSample.Conventions
{
public class EnableApiExplorerApplicationConvention : IApplicationModelConvention
{
public void Apply(ApplicationModel application)
{
application.ApiExplorer.IsVisible = true;
}
}
}

Mediante el uso de este enfoque (y de convenciones adicionales si es necesario), puede habilitar o deshabilitar las
visibilidad de API en cualquier nivel dentro de la aplicación.
Filtros en ASP.NET Core
31/05/2018 • 38 minutes to read • Edit Online

Por Rick Anderson, Tom Dykstra y Steve Smith


Los filtros en ASP.NET Core MVC permiten ejecutar código antes o después de determinadas fases de la
canalización de procesamiento de la solicitud.

IMPORTANT
Este tema no es válido con páginas de Razor. ASP.NET Core 2.1 y versiones posteriores admiten IPageFilter e
IAsyncPageFilter para las páginas de Razor. Para más información, vea Filter methods for Razor Pages (Métodos de
filtrado para páginas de Razor).

Los filtros integrados se encargan de tareas como las siguientes:


Autorización (impedir el acceso a los recursos a un usuario que no está autorizado).
Procurar que se use HTTPS en todas las solicitudes.
Almacenamiento en caché de respuestas (cortocircuitar la canalización de solicitud para devolver una
respuesta almacenada en caché).
Se pueden crear filtros personalizados que se encarguen de cuestiones transversales. Los filtros pueden
evitar la duplicación de código en las acciones. Así, por ejemplo, un filtro de excepción de control de errores
puede consolidar el control de errores.
Vea o descargue el ejemplo de GitHub.

¿Cómo funcionan los filtros?


Los filtros se ejecutan dentro de la canalización de invocación de acción de MVC, a veces denominada
canalización de filtro. La canalización de filtro se ejecuta después de que MVC seleccione la acción que se va
a ejecutar.
Tipos de filtro
Cada tipo de filtro se ejecuta en una fase diferente dentro de la canalización de filtro.
Los filtros de autorización se ejecutan en primer lugar y sirven para averiguar si el usuario actual está
autorizado para realizar la solicitud actual. Esos filtros pueden cortocircuitar la canalización si una
solicitud no está autorizada.
Los filtros de recursos son los primeros en atender la solicitud después de que se haya autorizado.
Pueden ejecutar código antes de pasar al resto de la canalización de filtro y después de que esta se
haya completado. Son útiles para implementar el almacenamiento en caché o para cortocircuitar la
canalización de filtro por motivos de rendimiento. Se ejecutan antes del enlace de modelos, de modo
que pueden influir en el enlace de modelos.
Los filtros de acciones pueden ejecutar código inmediatamente antes y después de llamar a un
método de acción individual. Se pueden usar para manipular los argumentos pasados a una acción y
el resultado obtenido de la acción.
Los filtros de excepciones sirven para aplicar directivas globales a las excepciones no controladas que
se producen antes de que se escriba algo en el cuerpo de respuesta.
Los filtros de resultados pueden ejecutar código inmediatamente antes y después de la ejecución de
resultados de acción individuales. Se ejecutan solo cuando el método de acción se ha ejecutado
correctamente. Son útiles para la lógica que debe regir la ejecución de la vista o el formateador.
En el siguiente diagrama se muestra cómo interactúan estos tipos de filtro en la canalización de filtro.
Implementación
Los filtros admiten implementaciones tanto sincrónicas como asincrónicas a través de diferentes
definiciones de interfaz.
Los filtros sincrónicos que pueden ejecutar código tanto antes como después de su fase de canalización
definen los métodos OnStageExecuting y OnStageExecuted. Así, por ejemplo, se llama a OnActionExecuting
antes de llamar al método de acción y a OnActionExecuted , después de que el método de acción haya vuelto.

using FiltersSample.Helper;
using Microsoft.AspNetCore.Mvc.Filters;

namespace FiltersSample.Filters
{
public class SampleActionFilter : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
// do something before the action executes
}

public void OnActionExecuted(ActionExecutedContext context)


{
// do something after the action executes
}
}
}

Los filtros asincrónicos definen un solo método OnStageExecutionAsync. Este método toma un delegado
FilterTypeExecutionDelegate que ejecuta la fase de canalización del filtro. Por ejemplo,
ActionExecutionDelegate llama al método de acción o al siguiente filtro de acción y el usuario puede ejecutar
código antes y después de esta llamada.
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Filters;

namespace FiltersSample.Filters
{
public class SampleAsyncActionFilter : IAsyncActionFilter
{
public async Task OnActionExecutionAsync(
ActionExecutingContext context,
ActionExecutionDelegate next)
{
// do something before the action executes
var resultContext = await next();
// do something after the action executes; resultContext.Result will be set
}
}
}

Se pueden implementar interfaces que abarquen varias fases de filtro en una sola clase. Por ejemplo, la clase
ActionFilterAttribute implementa IActionFilter e IResultFilter , así como sus equivalentes asincrónicos.

NOTE
Implemente la versión sincrónica o la versión asincrónica de una interfaz de filtro, pero no ambas. El marco
comprueba primero si el filtro implementa la interfaz asincrónica y, si es así, es a la interfaz que llama. De lo contrario,
llamará a métodos de interfaz sincrónicos. Si se implementaran ambas interfaces en una clase, solamente se llamaría al
método asincrónico. Cuando se usan clases abstractas como ActionFilterAttribute, se invalidan solo los métodos
sincrónicos o el método asincrónico de cada tipo de filtro.

IFilterFactory
IFilterFactory implementa IFilter . Por tanto, una instancia de IFilterFactory se puede usar como una
instancia de IFilter en cualquier parte de la canalización de filtro. Cuando el marco se prepara para
invocar el filtro, intenta convertirlo a un IFilterFactory . Si esa conversión se realiza correctamente, se llama
al método CreateInstance para crear la instancia IFilter que se va a invocar. Esto proporciona un diseño
flexible, dado que no hay que establecer la canalización de filtro exacta de forma explícita cuando la
aplicación se inicia.
Puede implementar IFilterFactory en sus propias implementaciones de atributo como método alternativo
para crear filtros:
public class AddHeaderWithFactoryAttribute : Attribute, IFilterFactory
{
// Implement IFilterFactory
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
{
return new InternalAddHeaderFilter();
}

private class InternalAddHeaderFilter : IResultFilter


{
public void OnResultExecuting(ResultExecutingContext context)
{
context.HttpContext.Response.Headers.Add(
"Internal", new string[] { "Header Added" });
}

public void OnResultExecuted(ResultExecutedContext context)


{
}
}

public bool IsReusable


{
get
{
return false;
}
}
}

Atributos de filtros integrados


El marco incluye filtros integrados basados en atributos que se pueden personalizar y a partir de los cuales
crear subclases. Por ejemplo, el siguiente filtro de resultados agrega un encabezado a la respuesta.

using Microsoft.AspNetCore.Mvc.Filters;

namespace FiltersSample.Filters
{
public class AddHeaderAttribute : ResultFilterAttribute
{
private readonly string _name;
private readonly string _value;

public AddHeaderAttribute(string name, string value)


{
_name = name;
_value = value;
}

public override void OnResultExecuting(ResultExecutingContext context)


{
context.HttpContext.Response.Headers.Add(
_name, new string[] { _value });
base.OnResultExecuting(context);
}
}
}

Los atributos permiten a los filtros aceptar argumentos, como se muestra en el ejemplo anterior. Podríamos
agregar este atributo a un método de acción o controlador, y especificar el nombre y el valor del encabezado
HTTP:
[AddHeader("Author", "Steve Smith @ardalis")]
public class SampleController : Controller
{
public IActionResult Index()
{
return Content("Examine the headers using developer tools.");
}

[ShortCircuitingResourceFilter]
public IActionResult SomeResource()
{
return Content("Successful access to resource - header should be set.");
}

Aquí se muestra el resultado de la acción Index ; los encabezados de respuesta aparecen en la parte inferior
derecha.

Algunas de las interfaces de filtro tienen atributos correspondientes que se pueden usar como clases base en
las implementaciones personalizadas.
Atributos de filtro:
ActionFilterAttribute
ExceptionFilterAttribute
ResultFilterAttribute
FormatFilterAttribute
ServiceFilterAttribute
TypeFilterAttribute

TypeFilterAttribute y ServiceFilterAttribute se explican más adelante en este artículo.

Ámbitos del filtro y orden de ejecución


Un filtro se puede agregar a la canalización en uno de tres ámbitos posibles. Un filtro se puede agregar a un
método de acción concreto o a una clase de controlador usando un atributo. Un filtro también se puede
registrar globalmente para todos los controladores y acciones. Para ello, solo hay que agregarlo a la
colección MvcOptions.Filters en ConfigureServices :
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(options =>
{
options.Filters.Add(new AddHeaderAttribute("GlobalAddHeader",
"Result filter added to MvcOptions.Filters")); // an instance
options.Filters.Add(typeof(SampleActionFilter)); // by type
options.Filters.Add(new SampleGlobalActionFilter()); // an instance
});

services.AddScoped<AddHeaderFilterWithDi>();
}

Orden de ejecución predeterminado


Cuando hay varios filtros en una determinada fase de la canalización, el ámbito determina el orden
predeterminado en el que esos filtros se van a ejecutar. Los filtros globales abarcan a los filtros de clase, que
a su vez engloban a los filtros de método. Este proceso se denomina en ocasiones anidamiento tipo
"matrioshka", ya que cada aumento en el ámbito está incluido en el ámbito anterior, como las muñecas rusas
matrioshka. Por lo general, se puede lograr el comportamiento de invalidación deseado sin tener que
especificar el orden de forma explícita.
Como resultado de este anidamiento, el código de filtros posterior se ejecuta en el orden inverso al código
anterior. La secuencia sería la siguiente:
Código de filtros anterior aplicado globalmente
Código de filtros anterior aplicado a los controladores
Código de filtros anterior aplicado a los métodos de acción
Código de filtros posterior aplicado a los métodos de acción
Código de filtros posterior aplicado a los controladores
Código de filtros posterior aplicado globalmente
Este es un ejemplo que ilustra el orden en el que se llama a los métodos de filtro relativos a filtros de
acciones sincrónicos.

SECUENCIA ÁMBITO DEL FILTRO MÉTODO DE FILTRO

1 Global OnActionExecuting

2 Controlador OnActionExecuting

3 Método OnActionExecuting

4 Método OnActionExecuted

5 Controlador OnActionExecuted

6 Global OnActionExecuted

Esta secuencia pone de manifiesto lo siguiente:


El filtro de método está anidado en el filtro de controlador.
El filtro de controlador está anidado en el filtro global.
Dicho de otro modo, si estamos en el método OnStageExecutionAsync de un filtro asincrónico, todos los
filtros que tengan un ámbito más restrictivo se ejecutan mientras el código está en la pila.
NOTE
Todos los controladores que heredan de la clase base Controller incluyen los métodos OnActionExecuting y
OnActionExecuted . Estos métodos incluyen los filtros que se ejecutan en relación con una acción determinada: se
llama a OnActionExecuting antes de cualquiera de los filtros y a OnActionExecuted , después de todos los filtros.

Invalidación del orden predeterminado


La secuencia de ejecución predeterminada se puede invalidar implementando IOrderedFilter . Esta interfaz
expone una propiedad Order que tiene prioridad sobre el ámbito a la hora de determinar el orden de
ejecución. Así, el código anterior de un filtro con un valor de Order más bajo se ejecutará antes que el de un
filtro con un valor de Order más alto, de igual modo que el código posterior de un filtro con un valor de
Order más bajo se ejecutará después que el de un filtro con un valor de Order más alto. La propiedad
Order se puede establecer por medio de un parámetro de constructor:

[MyFilter(Name = "Controller Level Attribute", Order=1)]

Si tuviéramos estos tres mismos filtros de acciones del ejemplo anterior, pero estableciéramos la propiedad
Order de los filtros global y del controlador en 1 y 2 respectivamente, el orden de ejecución se invertiría.

SECUENCIA ÁMBITO DEL FILTRO PROPIEDAD ORDER MÉTODO DE FILTRO

1 Método 0 OnActionExecuting

2 Controlador 1 OnActionExecuting

3 Global 2 OnActionExecuting

4 Global 2 OnActionExecuted

5 Controlador 1 OnActionExecuted

6 Método 0 OnActionExecuted

La propiedad Order altera el ámbito al determinar el orden en el que se ejecutarán los filtros. Los filtros se
clasifican por orden en primer lugar y, después, se usa el ámbito para priorizar en caso de igualdad. Todos
los filtros integrados implementan IOrderedFilter y establecen el valor predeterminado de Order en 0. En
los filtros integrados, el ámbito determinará el orden, a menos que Order se establezca en un valor distinto
de cero.

Cancelación y cortocircuito
La canalización de filtro se puede cortocircuitar en cualquier momento estableciendo la propiedad Result
en el parámetro context que se ha proporcionado al método de filtro. Por ejemplo, el siguiente filtro de
recursos impide que el resto de la canalización se ejecute.
using System;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;

namespace FiltersSample.Filters
{
public class ShortCircuitingResourceFilterAttribute : Attribute,
IResourceFilter
{
public void OnResourceExecuting(ResourceExecutingContext context)
{
context.Result = new ContentResult()
{
Content = "Resource unavailable - header should not be set"
};
}

public void OnResourceExecuted(ResourceExecutedContext context)


{
}
}
}

En el siguiente código, tanto el filtro ShortCircuitingResourceFilter como el filtro AddHeader tienen como
destino el método de acción SomeResource . El ShortCircuitingResourceFilter :
Se ejecuta en primer lugar, porque es un filtro de recursos y AddHeader es un filtro de acciones.
Cortocircuita el resto de la canalización.
Por tanto, el filtro AddHeader nunca se ejecuta en relación con la acción SomeResource . Este comportamiento
sería el mismo si ambos filtros se aplicaran en el nivel de método de acción, siempre y cuando
ShortCircuitingResourceFilter se haya ejecutado primero. ShortCircuitingResourceFilter se ejecuta
primero debido a su tipo de filtro o al uso explícito de la propiedad Order .

[AddHeader("Author", "Steve Smith @ardalis")]


public class SampleController : Controller
{
public IActionResult Index()
{
return Content("Examine the headers using developer tools.");
}

[ShortCircuitingResourceFilter]
public IActionResult SomeResource()
{
return Content("Successful access to resource - header should be set.");
}

Inserción de dependencias
Los filtros se pueden agregar por tipo o por instancia. Si agrega una instancia, dicha instancia se usará en
cada solicitud. Si se agrega un tipo, se activará por tipo, lo que significa que se creará una instancia por cada
solicitud y las dependencias de constructor que haya se rellenarán por medio de la inserción de
dependencias. Agregar un filtro por tipo equivale a usar
filters.Add(new TypeFilterAttribute(typeof(MyFilter))) .

Los filtros que se implementan como atributos y se agregan directamente a las clases de controlador o a los
métodos de acción no pueden tener dependencias de constructor proporcionadas por la inserción de
dependencias. El motivo es que los atributos deben tener los parámetros de constructor proporcionados allá
donde se apliquen. Se trata de una limitación de cómo funcionan los atributos.
Si los filtros tienen dependencias a las que hay que tener acceso desde la inserción de dependencias, existen
varios métodos posibles para ello. Puede aplicar el filtro a una clase o a un método de acción usando
cualquiera de los siguientes elementos:
ServiceFilterAttribute
TypeFilterAttribute
IFilterFactory implementado en el atributo

NOTE
Una dependencia que probablemente convenga obtener de la inserción de dependencias es un registrador. Pese a ello,
no cree ni use filtros con fines exclusivamente de registro, ya que seguro que las características de registro del marco
integradas ya proporcionan lo que necesita. Si va a agregar funciones de registro a los filtros, estas deberán ir dirigidas
a cuestiones de dominio empresarial o a un comportamiento específico del filtro, y no a acciones de MVC o a otros
eventos del marco.

ServiceFilterAttribute
ServiceFilter recupera una instancia del filtro de la inserción de dependencias. Para ello, se agrega el filtro
en cuestión al contenedor de ConfigureServices y se hace referencia a él en un atributo ServiceFilter .

public void ConfigureServices(IServiceCollection services)


{
services.AddMvc(options =>
{
options.Filters.Add(new AddHeaderAttribute("GlobalAddHeader",
"Result filter added to MvcOptions.Filters")); // an instance
options.Filters.Add(typeof(SampleActionFilter)); // by type
options.Filters.Add(new SampleGlobalActionFilter()); // an instance
});

services.AddScoped<AddHeaderFilterWithDi>();
}

[ServiceFilter(typeof(AddHeaderFilterWithDi))]
public IActionResult Index()
{
return View();
}

Si ServiceFilter se usa sin registrar el tipo de filtro, se producirá una excepción:

System.InvalidOperationException: No service for type


'FiltersSample.Filters.AddHeaderFilterWithDI' has been registered.

ServiceFilterAttribute implementa IFilterFactory . IFilterFactory expone el método CreateInstance


para crear una instancia de IFilter . El método CreateInstance carga el tipo especificado desde el
contenedor de servicios (inserción de dependencias).
TypeFilterAttribute
TypeFilterAttribute es similar a ServiceFilterAttribute , pero su tipo no se resuelve directamente desde el
contenedor de inserción de dependencias, sino que crea una instancia del tipo usando el elemento
Microsoft.Extensions.DependencyInjection.ObjectFactory .
Debido a esta diferencia:
Los tipos a los que se hace referencia con TypeFilterAttribute no tienen que estar ya registrados con el
contenedor. Sus dependencias se completan a través del contenedor.
TypeFilterAttribute puede aceptar opcionalmente argumentos de constructor del tipo en cuestión.

En el siguiente ejemplo se muestra cómo pasar argumentos a un tipo usando TypeFilterAttribute :

[TypeFilter(typeof(AddHeaderAttribute),
Arguments = new object[] { "Author", "Steve Smith (@ardalis)" })]
public IActionResult Hi(string name)
{
return Content($"Hi {name}");
}

Si tiene un filtro con las siguientes características:


No necesita argumentos.
Tiene dependencias de constructor que deben completarse por medio de la inserción de dependencias.
Puede usar su propio atributo con nombre en las clases y métodos, en lugar de
[TypeFilter(typeof(FilterType))] ). En el siguiente filtro se muestra cómo se puede implementar esto:

public class SampleActionFilterAttribute : TypeFilterAttribute


{
public SampleActionFilterAttribute():base(typeof(SampleActionFilterImpl))
{
}

private class SampleActionFilterImpl : IActionFilter


{
private readonly ILogger _logger;
public SampleActionFilterImpl(ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger<SampleActionFilterAttribute>();
}

public void OnActionExecuting(ActionExecutingContext context)


{
_logger.LogInformation("Business action starting...");
// perform some business logic work

public void OnActionExecuted(ActionExecutedContext context)


{
// perform some business logic work
_logger.LogInformation("Business action completed.");
}
}
}

Este filtro se puede aplicar a clases o a métodos usando la sintaxis de [SampleActionFilter] , en lugar de
tener que recurrir a [TypeFilter] o a [ServiceFilter] .

Filtros de autorización
Los *filtros de autorización:
Controlan el acceso a los métodos de acción.
Son los primeros filtros que se ejecutan en la canalización de filtro.
Tienen un método anterior, pero no uno posterior.
Solo se deben escribir filtros de autorización personalizados si se está escribiendo un marco de autorización
propio. Es preferible configurar directivas de autorización o escribir una directiva de autorización
personalizada a escribir un filtro personalizado. La implementación de filtro integrada se encarga
únicamente de llamar al sistema de autorización.
No se deberían producir excepciones dentro de los filtros de autorización, ya que no habrá nada que
controle esas excepciones (los filtros de excepciones no lo harán). Considere la posibilidad de emitir un
desafío cuando se produzca una excepción.
Aquí encontrará más información sobre la autorización.

Filtros de recursos
Implementan la interfaz IResourceFilter o IAsyncResourceFilter .
Su ejecución abarca la mayor parte de la canalización de filtro.
Los filtros de autorización son los únicos que se ejecutan antes que los filtros de recursos.
Los filtros de recursos son útiles para cortocircuitar la mayor parte del trabajo que está realizando una
solicitud. Por ejemplo, un filtro de almacenamiento en caché puede evitar que se ejecute el resto de la
canalización si la respuesta está en la memoria caché.
El filtro de recursos de cortocircuito mostrado anteriormente es un ejemplo de filtro de recursos. Otro
ejemplo es DisableFormValueModelBindingAttribute:
Evita que el enlace de modelos tenga acceso a los datos del formulario.
Resulta práctico cuando hay cargas de archivos muy voluminosos y se quiere impedir que el formulario
se lea en la memoria.

Filtros de acciones
Los filtros de acciones:
Implementan la interfaz IActionFilter o IAsyncActionFilter .
Su ejecución rodea la ejecución de los métodos de acción.
Este es un filtro de acciones de ejemplo:

public class SampleActionFilter : IActionFilter


{
public void OnActionExecuting(ActionExecutingContext context)
{
// do something before the action executes
}

public void OnActionExecuted(ActionExecutedContext context)


{
// do something after the action executes
}
}

ActionExecutingContext proporciona las siguientes propiedades:


ActionArguments : permite manipular las entradas en la acción.
Controller : permite manipular la instancia del controlador.
Result : si se establece, cortocircuita la ejecución del método de acción y de los filtros de acciones
posteriores. Producir una excepción también impide que el método de acción y los filtros posteriores se
ejecuten, pero se considera un error en vez de un resultado correcto.
ActionExecutedContext proporciona Controller y Result , además de las siguientes propiedades:
Canceled : será true si otro filtro ha cortocircuitado la ejecución de la acción.
Exception : será distinto de null si la acción o un filtro de acción posterior han producido una excepción.
Si esta propiedad se establece en null, se "controlará" una excepción de forma eficaz y Result se
ejecutará como si se hubiera devuelto desde el método de acción con normalidad.
En un IAsyncActionFilter , una llamada a ActionExecutionDelegate :
Ejecuta cualquier filtro de acciones posterior y el método de acción.
Devuelve ActionExecutedContext .
Para cortocircuitar esto, asigne ActionExecutingContext.Result a alguna instancia de resultado y no llame a
ActionExecutionDelegate .

El marco proporciona una clase abstracta ActionFilterAttribute de la que se pueden crear subclases.
Un filtro de acciones puede servir para validar el estado del modelo y devolver cualquier error que surja si el
estado no es válido:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;

namespace FiltersSample.Filters
{
public class ValidateModelAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
context.Result = new BadRequestObjectResult(context.ModelState);
}
}
}
}

El método OnActionExecuted se ejecuta después del método de acción y puede ver y manipular los
resultados de la acción a través de la propiedad ActionExecutedContext.Result .
ActionExecutedContext.Canceled se establecerá en true si otro filtro ha cortocircuitado la ejecución de la
acción. ActionExecutedContext.Exception se establecerá en un valor distinto de null si la acción o un filtro de
acción posterior han producido una excepción. Si ActionExecutedContext.Exception se establece como nulo:
Controla una excepción eficazmente.
ActionExectedContext.Result se ejecuta como si se devolviera con normalidad desde el método de
acción.

Filtros de excepciones
Los filtros de excepciones implementan la interfaz IExceptionFilter o IAsyncExceptionFilter . Se pueden
usar para implementar directivas de control de errores comunes de una aplicación.
En el siguiente filtro de excepciones de ejemplo se usa una vista de error de desarrollador personalizada
para mostrar los detalles sobre las excepciones que se producen cuando la aplicación está en fase de
desarrollo:

public class CustomExceptionFilterAttribute : ExceptionFilterAttribute


{
private readonly IHostingEnvironment _hostingEnvironment;
private readonly IModelMetadataProvider _modelMetadataProvider;

public CustomExceptionFilterAttribute(
IHostingEnvironment hostingEnvironment,
IModelMetadataProvider modelMetadataProvider)
{
_hostingEnvironment = hostingEnvironment;
_modelMetadataProvider = modelMetadataProvider;
}

public override void OnException(ExceptionContext context)


{
if (!_hostingEnvironment.IsDevelopment())
{
// do nothing
return;
}
var result = new ViewResult {ViewName = "CustomError"};
result.ViewData = new ViewDataDictionary(_modelMetadataProvider,context.ModelState);
result.ViewData.Add("Exception", context.Exception);
// TODO: Pass additional detailed data via ViewData
context.Result = result;
}
}

Los filtros de excepciones:


No tienen eventos anteriores ni posteriores.
Implementan OnException o OnExceptionAsync .
Controlan las excepciones sin controlar que se producen al crear controladores, en el enlace de modelos,
en los filtros de acciones o en los métodos de acción.
No detectan aquellas excepciones que se produzcan en los filtros de recursos, en los filtros de resultados
o en la ejecución de resultados de MVC.
Para controlar una excepción, establezca la propiedad ExceptionContext.ExceptionHandled en true o escriba
una respuesta. Esto detiene la propagación de la excepción. Un filtro de excepciones no tiene capacidad para
convertir una excepción en un proceso "correcto". Eso solo lo pueden hacer los filtros de acciones.

NOTE
En ASP.NET Core 1.1, la respuesta no se envía si ExceptionHandled está establecido en true y escribe una respuesta.
En este caso, ASP.NET Core 1.0 envía la respuesta, mientras que ASP.NET Core 1.1.2 regresará al comportamiento de la
versión 1.0. Para más información, vea el problema n.º 5594 del repositorio de GitHub.

Los filtros de excepciones:


Son adecuados para interceptar las excepciones que se producen en las acciones de MVC.
No son tan flexibles como el middleware de control de errores.
Es preferible usar middleware de control de excepciones. Use filtros de excepción solo cuando deba realizar
el control de errores de manera diferente según la acción de MVC elegida. Por ejemplo, puede que su
aplicación tenga métodos de acción tanto para los puntos de conexión de API como para las vistas/HTML.
Los puntos de conexión de API podrían devolver información sobre errores como JSON, mientras que las
acciones basadas en vistas podrían devolver una página de error como HTML.
ExceptionFilterAttribute puede tener subclases.

Filtros de resultados
Implementan la interfaz IResultFilter o IAsyncResultFilter .
Su ejecución rodea la ejecución de los resultados de acción.
Este es un ejemplo de un filtro de resultados que agrega un encabezado HTTP.

public class AddHeaderFilterWithDi : IResultFilter


{
private ILogger _logger;
public AddHeaderFilterWithDi(ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger<AddHeaderFilterWithDi>();
}

public void OnResultExecuting(ResultExecutingContext context)


{
var headerName = "OnResultExecuting";
context.HttpContext.Response.Headers.Add(
headerName, new string[] { "ResultExecutingSuccessfully" });
_logger.LogInformation($"Header added: {headerName}");
}

public void OnResultExecuted(ResultExecutedContext context)


{
// Can't add to headers here because response has already begun.
}
}

El tipo de resultado que se ejecute dependerá de la acción en cuestión. Una acción de MVC que devuelve
una vista incluye todo el procesamiento de Razor como parte del elemento ViewResult que se está
ejecutando. Un método API puede llevar a cabo algunas funciones de serialización como parte de la
ejecución del resultado. Aquí encontrará más información sobre los resultados de acciones.
Los filtros de resultados solo se ejecutan cuando los resultados son correctos; es decir, cuando la acción o los
filtros de acciones generan un resultado de acción. Los filtros de resultados no se ejecutan si hay filtros de
excepciones que controlan una excepción.
El método OnResultExecuting puede cortocircuitar la ejecución del resultado de la acción y de los filtros de
resultados posteriores estableciendo ResultExecutingContext.Cancel en true. Por lo general, conviene
escribir en el objeto de respuesta cuando el proceso se cortocircuite, ya que así evitará que se genere una
respuesta vacía. Si se produce una excepción, sucederá lo siguiente:
Se impedirá la ejecución del resultado de la acción y de los filtros subsiguientes.
Se tratará como un error en lugar de como un resultado correcto.
Cuando el método OnResultExecuted se ejecuta, probablemente la respuesta se haya enviado al cliente y ya
no se pueda cambiar (a menos que se produzca una excepción). ResultExecutedContext.Canceled se
establecerá en true si otro filtro ha cortocircuitado la ejecución del resultado de la acción.
ResultExecutedContext.Exception se establecerá en un valor distinto de null si el resultado de la acción o un
filtro de resultado posterior ha producido una excepción. Establecer Exception en un valor null hace que
una excepción se "controle" de forma eficaz y evita que MVC vuelva a producir dicha excepción más
adelante en la canalización. Cuando se controla una excepción en un filtro de resultados, no se pueden
escribir datos en la respuesta. Si el resultado de la acción produce una excepción a mitad de su ejecución y
los encabezados ya se han vaciado en el cliente, no hay ningún mecanismo confiable que permita enviar un
código de error.
En un elemento IAsyncResultFilter , una llamada a await next en ResultExecutionDelegate ejecuta
cualquier filtro de resultados posterior y el resultado de la acción. Para cortocircuitar esto, establezca
ResultExecutingContext.Cancel en true y no llame a ResultExectionDelegate .

El marco proporciona una clase abstracta ResultFilterAttribute de la que se pueden crear subclases. La
clase AddHeaderAttribute mostrada anteriormente es un ejemplo de un atributo de filtro de resultados.

Uso de middleware en la canalización de filtro


Los filtros de recursos funcionan como el middleware, en el sentido de que se encargan de la ejecución de
todo lo que viene después en la canalización. Pero los filtros se diferencian del middleware en que forman
parte de MVC, lo que significa que tienen acceso al contexto y las construcciones de MVC.
En ASP.NET Core 1.1 se puede usar middleware en la canalización de filtro. Conviene hacerlo si tiene un
componente de middleware que necesita tener acceso a los datos de ruta de MVC, u otro que deba
ejecutarse solo con ciertos controladores o acciones.
Para usar middleware como un filtro, cree un tipo con un método Configure en el que se especifique el
middleware que quiera insertar en la canalización de filtro. Este es un ejemplo en el que se usa middleware
de localización para establecer la referencia cultural actual de una solicitud:

public class LocalizationPipeline


{
public void Configure(IApplicationBuilder applicationBuilder)
{
var supportedCultures = new[]
{
new CultureInfo("en-US"),
new CultureInfo("fr")
};

var options = new RequestLocalizationOptions


{

DefaultRequestCulture = new RequestCulture(culture: "en-US", uiCulture: "en-US"),


SupportedCultures = supportedCultures,
SupportedUICultures = supportedCultures
};
options.RequestCultureProviders = new[]
{ new RouteDataRequestCultureProvider() { Options = options } };

applicationBuilder.UseRequestLocalization(options);
}
}

Después, puede usar MiddlewareFilterAttribute para ejecutar el middleware en relación con una acción o
controlador concretos, o bien de manera global:

[Route("{culture}/[controller]/[action]")]
[MiddlewareFilter(typeof(LocalizationPipeline))]
public IActionResult CultureFromRouteData()
{
return Content($"CurrentCulture:{CultureInfo.CurrentCulture.Name},"
+ $"CurrentUICulture:{CultureInfo.CurrentUICulture.Name}");
}

Los filtros de middleware se ejecutan en la misma fase de la canalización de filtro que los filtros de recursos,
antes del enlace de modelos y después del resto de la canalización.
Siguientes acciones
Para experimentar con los filtros, descargue, pruebe y modifique este ejemplo.
Áreas de ASP.NET Core
25/06/2018 • 9 minutes to read • Edit Online

Por Dhananjay Kumar y Rick Anderson


Las áreas son una característica de MVC de ASP.NET que se usa para organizar funciones relacionadas en un
grupo como un espacio de nombres independiente (para el enrutamiento) y una estructura de carpetas (para las
vistas). El uso de áreas crea una jerarquía para el enrutamiento mediante la adición de otro parámetro de ruta,
area , a controller y action .

Las áreas ofrecen una manera de dividir una aplicación web ASP.NET Core MVC de gran tamaño en
agrupaciones funcionales más pequeñas. Un área es en realidad una estructura de MVC dentro de una
aplicación. En un proyecto de MVC, los componentes lógicos como el modelo, el controlador y la vista se
guardan en carpetas diferentes, y MVC usa las convenciones de nomenclatura para crear la relación entre estos
componentes. Para una aplicación grande, puede ser conveniente dividir la aplicación en distintas áreas de
funciones de alto nivel. Por ejemplo, una aplicación de comercio electrónico con varias unidades de negocio,
como la finalización de la compra, la facturación, la búsqueda, etc. Cada una de estas unidades tiene sus propias
vistas, controladores y modelos de componentes lógicos. En este escenario, puede usar las áreas para dividir
físicamente los componentes empresariales del mismo proyecto.
Un área puede definirse como unidades funcionales más pequeñas en un proyecto de ASP.NET Core MVC con
su propio conjunto de modelos, vistas y controladores.
Considere el uso de áreas en un proyecto de MVC en los casos siguientes:
La aplicación está formada por varios componentes funcionales de alto nivel que deben separarse
lógicamente.
Le interesa dividir el proyecto de MVC para que se pueda trabajar en cada área funcional de forma
independiente.
Características de las áreas:
Una aplicación ASP.NET Core MVC puede tener cualquier número de áreas.
Cada área tiene sus propios controladores, modelos y vistas.
Permiten organizar proyectos de MVC de gran tamaño en varios componentes de alto nivel en los que se
puede trabajar de forma independiente.
Admiten varios controladores con el mismo nombre, siempre y cuando tengan áreas diferentes.
Veamos un ejemplo para ilustrar cómo se crean y se usan las áreas. Supongamos que tiene una aplicación de
tienda con dos grupos distintos de controladores y vistas: Productos y Servicios. Una estructura de carpetas
típica para dicha aplicación con áreas de MVC tendría un aspecto similar al siguiente:
Nombre de proyecto
Áreas
Productos
Controladores
HomeController.cs
ManageController.cs
Vistas
Página principal
Index.cshtml
Administrar
Index.cshtml
Servicios
Controladores
HomeController.cs
Vistas
Página principal
Index.cshtml
Cuando MVC intenta representar una vista en un área, de forma predeterminada, busca en las ubicaciones
siguientes:

/Areas/<Area-Name>/Views/<Controller-Name>/<Action-Name>.cshtml
/Areas/<Area-Name>/Views/Shared/<Action-Name>.cshtml
/Views/Shared/<Action-Name>.cshtml

Estas son las ubicaciones predeterminadas que se pueden cambiar mediante AreaViewLocationFormats en
Microsoft.AspNetCore.Mvc.Razor.RazorViewEngineOptions .

Por ejemplo, en el código siguiente, el nombre de carpeta "Areas" se ha cambiado a "Categories".

services.Configure<RazorViewEngineOptions>(options =>
{
options.AreaViewLocationFormats.Clear();
options.AreaViewLocationFormats.Add("/Categories/{2}/Views/{1}/{0}.cshtml");
options.AreaViewLocationFormats.Add("/Categories/{2}/Views/Shared/{0}.cshtml");
options.AreaViewLocationFormats.Add("/Views/Shared/{0}.cshtml");
});

Hay que tener en cuenta que la estructura de la carpeta Views es la única que se considera importante aquí; el
contenido de las demás carpetas, como Controllers y Models, no importa. Por ejemplo, no hace falta tener una
carpeta Controllers y Models. Esto funciona porque el contenido de Controllers y Models solo es código que se
compila en una .dll, mientras que el contenido de Views no lo es mientras no se realice una solicitud a esa vista.
Una vez que haya definido la jerarquía de carpetas, debe indicar a MVC que cada controlador está asociado a un
área. Para hacerlo, decore el nombre del controlador con el atributo [Area] .
...
namespace MyStore.Areas.Products.Controllers
{
[Area("Products")]
public class HomeController : Controller
{
// GET: /Products/Home/Index
public IActionResult Index()
{
return View();
}

// GET: /Products/Home/Create
public IActionResult Create()
{
return View();
}
}
}

Establezca una definición de ruta que funcione con las áreas recién creadas. En el artículo Enrutamiento a
acciones del controlador se explica en detalle cómo crear definiciones de ruta, incluido el uso de rutas
convencionales frente a rutas de atributo. En este ejemplo usaremos una ruta convencional. Para ello, abra el
archivo Startup.cs y modifíquelo mediante la adición de la definición de ruta con nombre areaRoute que se
indica a continuación.

...
app.UseMvc(routes =>
{
routes.MapRoute(
name: "areaRoute",
template: "{area:exists}/{controller=Home}/{action=Index}/{id?}");

routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});

Si va a http://<yourApp>/products , se invocará el método de acción Index de HomeController en el área


Products .

Generación de vínculos
Generación de vínculos desde una acción dentro de un controlador basado en área en otra acción dentro
del mismo controlador.
Supongamos que la ruta de la solicitud actual es /Products/Home/Create .
Sintaxis de HtmlHelper: @Html.ActionLink("Go to Product's Home Page", "Index")

Sintaxis de TagHelper: <a asp-action="Index">Go to Product's Home Page</a>

Tenga en cuenta que es necesario proporcionar aquí los valores de área y controlador, dado que ya están
disponibles en el contexto de la solicitud actual. Estos tipos de valores se denominan valores ambient .
Generación de vínculos desde una acción dentro de un controlador basado en área en otra acción en un
controlador diferente.
Supongamos que la ruta de la solicitud actual es /Products/Home/Create .
Sintaxis de HtmlHelper: @Html.ActionLink("Go to Manage Products Home Page", "Index", "Manage")

Sintaxis de TagHelper:
<a asp-controller="Manage" asp-action="Index">Go to Manage Products Home Page</a>

Tenga en cuenta que aquí se usa el valor de ambiente de área, pero el valor de controlador se especifica
explícitamente más arriba.
Generación de vínculos desde una acción dentro de un controlador basado en área en otra acción en un
controlador diferente y un área diferente.
Supongamos que la ruta de la solicitud actual es /Products/Home/Create .
Sintaxis de HtmlHelper:
@Html.ActionLink("Go to Services Home Page", "Index", "Home", new { area = "Services" })

Sintaxis de TagHelper:
<a asp-area="Services" asp-controller="Home" asp-action="Index">Go to Services Home Page</a>

Tenga en cuenta que aquí no se usan valores de ambiente.


Generación de vínculos desde una acción dentro de un controlador basado en área en otra acción en un
controlador diferente y no en un área.
Sintaxis de HtmlHelper:
@Html.ActionLink("Go to Manage Products Home Page", "Index", "Home", new { area = "" })

Sintaxis de TagHelper:
<a asp-area="" asp-controller="Manage" asp-action="Index">Go to Manage Products Home Page</a>

Dado que queremos generar vínculos en una acción de controlador que no está basada en un área,
vaciaremos aquí el valor de ambiente para el área.

Publicación de áreas
Todos los archivos *.cshtml y wwwroot/** se publican en la salida cuando se incluye
<Project Sdk="Microsoft.NET.Sdk.Web"> en el archivo .csproj.
Elementos de aplicación en ASP.NET Core
25/06/2018 • 7 minutes to read • Edit Online

Vea o descargue el código de ejemplo (cómo descargarlo)


Un elemento de aplicación es una abstracción sobre los recursos de una aplicación desde el que se pueden
detectar características de MVC como controladores, componentes de vista o aplicaciones auxiliares de etiquetas.
Un ejemplo de un elemento de aplicación es AssemblyPart, que encapsula una referencia de ensamblado y expone
los tipos y las referencias de la compilación. Los proveedores de características trabajan con los elementos de
aplicación para rellenar las características de una aplicación de ASP.NET Core MVC. El uso principal de los
elementos de aplicación es permitir la configuración de la aplicación para detectar características MVC (o evitar su
carga) de un ensamblado.

Introducción a los elementos de aplicación


Las aplicaciones MVC cargan sus características desde elementos de aplicación. En particular, la clase
AssemblyPart representa un elemento de aplicación que está respaldado por un ensamblado. Puede utilizar estas
clases para detectar y cargar las características MVC, tales como controladores, componentes de vista, aplicaciones
auxiliares de etiquetas y orígenes de compilación de Razor. ApplicationPartManager es responsable del
seguimiento de los elementos de aplicación y los proveedores de características disponibles para la aplicación
MVC. Puede interactuar con ApplicationPartManager en Startup cuando configura MVC:

// create an assembly part from a class's assembly


var assembly = typeof(Startup).GetTypeInfo().Assembly;
services.AddMvc()
.AddApplicationPart(assembly);

// OR
var assembly = typeof(Startup).GetTypeInfo().Assembly;
var part = new AssemblyPart(assembly);
services.AddMvc()
.ConfigureApplicationPartManager(apm => apm.ApplicationParts.Add(part));

De forma predeterminada, MVC examinará el árbol de dependencias y buscará controladores (incluso en otros
ensamblados). Para cargar un ensamblado arbitrario (por ejemplo, desde un complemento al que no se hace
referencia en tiempo de compilación), puede utilizar un elemento de aplicación.
Los elementos de aplicación se pueden usar para evitar tener que buscar controladores en una determinada
ubicación o ensamblado. Modifique la colección ApplicationParts de ApplicationPartManager para controlar qué
elementos (o ensamblados) están disponibles para la aplicación. El orden de las entradas de la colección
ApplicationParts es irrelevante. Es importante configurar totalmente ApplicationPartManager antes de usarlo para
configurar los servicios en el contenedor. Por ejemplo, debe configurar totalmente ApplicationPartManager antes
de invocar a AddControllersAsServices . Si no lo hace, significará que los controladores de elementos de aplicación
que se han agregado después de esa llamada al método no se verán afectados (no se registrarán como servicios),
lo que podría dar lugar a un comportamiento incorrecto de la aplicación.
Si tiene un ensamblado que contiene controladores que no quiere usar, quítelo de ApplicationPartManager :
services.AddMvc()
.ConfigureApplicationPartManager(apm =>
{
var dependentLibrary = apm.ApplicationParts
.FirstOrDefault(part => part.Name == "DependentLibrary");

if (dependentLibrary != null)
{
p.ApplicationParts.Remove(dependentLibrary);
}
})

Además del ensamblado del proyecto y sus ensamblados dependientes, ApplicationPartManager incluirá
elementos de Microsoft.AspNetCore.Mvc.TagHelpers y Microsoft.AspNetCore.Mvc.Razor de forma predeterminada.

Proveedores de características de la aplicación


Los proveedores de características de la aplicación examinan los elementos de aplicación y proporcionan
características para esos elementos. Existen proveedores de características integrados para las siguientes
características MVC:
Controladores
Referencia de los metadatos
Aplicaciones auxiliares de etiquetas
Componentes de vista
Los proveedores de características heredan de IApplicationFeatureProvider<T> , donde T es el tipo de la
característica. Puede implementar sus propios proveedores de características para cualquiera de los tipos de
características de MVC mencionados anteriormente. El orden de los proveedores de características en la colección
ApplicationPartManager.FeatureProviders puede ser importante, puesto que los proveedores posteriores pueden
reaccionar ante las acciones realizadas por los proveedores anteriores.
Ejemplo: característica de controlador genérico
De forma predeterminada, ASP.NET Core MVC omite los controladores genéricos (por ejemplo,
SomeController<T> ). En este ejemplo se usa un proveedor de características de controlador que se ejecuta después
del proveedor predeterminado y agrega instancias de controlador genérico para una lista especificada de tipos
(definida en EntityTypes.Types ):

public class GenericControllerFeatureProvider : IApplicationFeatureProvider<ControllerFeature>


{
public void PopulateFeature(IEnumerable<ApplicationPart> parts, ControllerFeature feature)
{
// This is designed to run after the default ControllerTypeProvider,
// so the list of 'real' controllers has already been populated.
foreach (var entityType in EntityTypes.Types)
{
var typeName = entityType.Name + "Controller";
if (!feature.Controllers.Any(t => t.Name == typeName))
{
// There's no 'real' controller for this entity, so add the generic version.
var controllerType = typeof(GenericController<>)
.MakeGenericType(entityType.AsType()).GetTypeInfo();
feature.Controllers.Add(controllerType);
}
}
}
}
Tipos de entidad:

public static class EntityTypes


{
public static IReadOnlyList<TypeInfo> Types => new List<TypeInfo>()
{
typeof(Sprocket).GetTypeInfo(),
typeof(Widget).GetTypeInfo(),
};

public class Sprocket { }


public class Widget { }
}

El proveedor de características se agrega en Startup :

services.AddMvc()
.ConfigureApplicationPartManager(apm =>
apm.FeatureProviders.Add(new GenericControllerFeatureProvider()));

De forma predeterminada, los nombres de controlador genérico utilizados para el enrutamiento tendrían el
formato GenericController`1 [Widget] en lugar de Widget. El siguiente atributo se usa para modificar el nombre
para coincidir con el tipo genérico usado por el controlador:

using Microsoft.AspNetCore.Mvc.ApplicationModels;
using System;

namespace AppPartsSample
{
// Used to set the controller name for routing purposes. Without this convention the
// names would be like 'GenericController`1[Widget]' instead of 'Widget'.
//
// Conventions can be applied as attributes or added to MvcOptions.Conventions.
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public class GenericControllerNameConvention : Attribute, IControllerModelConvention
{
public void Apply(ControllerModel controller)
{
if (controller.ControllerType.GetGenericTypeDefinition() !=
typeof(GenericController<>))
{
// Not a GenericController, ignore.
return;
}

var entityType = controller.ControllerType.GenericTypeArguments[0];


controller.ControllerName = entityType.Name;
}
}
}

La clase GenericController :
using Microsoft.AspNetCore.Mvc;

namespace AppPartsSample
{
[GenericControllerNameConvention] // Sets the controller name based on typeof(T).Name
public class GenericController<T> : Controller
{
public IActionResult Index()
{
return Content($"Hello from a generic {typeof(T).Name} controller.");
}
}
}

Este es el resultado cuando se solicita una ruta coincidente:

Ejemplo: mostrar las características disponibles


Para iterar a través de las características rellenadas disponibles para la aplicación, puede solicitar un administrador
ApplicationPartManager mediante inserción de dependencias y usarlo para rellenar las instancias de las
características adecuadas:
using AppPartsSample.ViewModels;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.Mvc.Controllers;
using System.Linq;
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
using Microsoft.AspNetCore.Mvc.Razor.TagHelpers;
using Microsoft.AspNetCore.Mvc.ViewComponents;

namespace AppPartsSample.Controllers
{
public class FeaturesController : Controller
{
private readonly ApplicationPartManager _partManager;

public FeaturesController(ApplicationPartManager partManager)


{
_partManager = partManager;
}

public IActionResult Index()


{
var viewModel = new FeaturesViewModel();

var controllerFeature = new ControllerFeature();


_partManager.PopulateFeature(controllerFeature);
viewModel.Controllers = controllerFeature.Controllers.ToList();

var metaDataReferenceFeature = new MetadataReferenceFeature();


_partManager.PopulateFeature(metaDataReferenceFeature);
viewModel.MetadataReferences = metaDataReferenceFeature.MetadataReferences
.ToList();

var tagHelperFeature = new TagHelperFeature();


_partManager.PopulateFeature(tagHelperFeature);
viewModel.TagHelpers = tagHelperFeature.TagHelpers.ToList();

var viewComponentFeature = new ViewComponentFeature();


_partManager.PopulateFeature(viewComponentFeature);
viewModel.ViewComponents = viewComponentFeature.ViewComponents.ToList();

return View(viewModel);
}
}
}

Salida del ejemplo:


Enlace de modelos personalizado en ASP.NET Core
25/06/2018 • 12 minutes to read • Edit Online

Por Steve Smith


Con el enlace de modelos, las acciones de controlador pueden funcionar directamente con tipos de modelos
(pasados como argumentos de método), en lugar de con solicitudes HTTP. La asignación entre los datos de
solicitudes entrantes y los modelos de aplicaciones se controla por medio de enlazadores de modelos. Los
desarrolladores pueden ampliar la funcionalidad integrada de enlace de modelos implementando enlazadores de
modelos personalizados (si bien, por lo general, no es necesario escribir un proveedor propio).
Ver o descargar el ejemplo desde GitHub

Limitaciones de los enlazadores de modelos predeterminados


Los enlazadores de modelos predeterminados admiten la mayoría de los tipos de datos de .NET Core comunes y
deberían cubrir las necesidades de casi cualquier desarrollador. Esperan enlazar entradas basadas en texto desde
la solicitud directamente a tipos de modelos. Puede que sea necesario transformar la entrada antes de enlazarla.
Es el caso, por ejemplo, si tiene una clave que se puede usar para buscar datos del modelo. Se puede usar un
enlazador de modelos personalizado para capturar datos en función de la clave.

Revisión del enlace de modelos


El enlace de modelos usa definiciones específicas de los tipos con los que funciona. Un tipo simple se convierte a
partir de una sola cadena de la entrada, mientras que un tipo complejo se convierte a partir de varios valores de
entrada. El marco establece la diferencia dependiendo de si existe un TypeConverter . Conviene crear un
convertidor de tipos si existe una asignación simple string -> SomeType que no necesita recursos externos.
Antes de crear su propio enlazador de modelos personalizado, no está de más que repase cómo se implementan
los enlazadores de modelos existentes. Hay que considerar el uso de ByteArrayModelBinder, que sirve para
convertir cadenas codificadas con base64 en matrices de bytes. Las matrices de bytes se suelen almacenar como
archivos o como campos de tipo BLOB de base de datos.
Trabajar con ByteArrayModelBinder
Las cadenas codificadas con base64 se pueden usar para representar datos binarios. Por ejemplo, la siguiente
imagen se puede codificar como una cadena.

En la siguiente imagen se muestra una pequeña porción de la cadena codificada:


Siga las instrucciones del archivo Léame del ejemplo para convertir la cadena codificada con base64 en un
archivo.
ASP.NET Core MVC toma cadenas codificadas con base64 y usa un ByteArrayModelBinder para convertirlas en
una matriz de bytes. ByteArrayModelBinderProvider, que implementa IModelBinderProvider, asigna argumentos
byte[] a ByteArrayModelBinder :

public IModelBinder GetBinder(ModelBinderProviderContext context)


{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}

if (context.Metadata.ModelType == typeof(byte[]))
{
return new ByteArrayModelBinder();
}

return null;
}

Cuando cree su propio enlazador de modelos personalizado, puede implementar su tipo IModelBinderProvider
particular o usar ModelBinderAttribute.
En el siguiente ejemplo se indica cómo usar ByteArrayModelBinder para convertir una cadena codificada con
base64 en un byte[] y guardar el resultado en un archivo:
// POST: api/image
[HttpPost]
public void Post(byte[] file, string filename)
{
string filePath = Path.Combine(_env.ContentRootPath, "wwwroot/images/upload", filename);
if (System.IO.File.Exists(filePath)) return;
System.IO.File.WriteAllBytes(filePath, file);
}

Se puede usar un método API POST en una cadena codificada con base64 con una herramienta como Postman:

Siempre y cuando el enlazador pueda enlazar datos de la solicitud a argumentos o propiedades con el nombre
adecuado, el enlace de modelos se realizará correctamente. En el siguiente ejemplo se muestra cómo usar
ByteArrayModelBinder con un modelo de vista:

[HttpPost("Profile")]
public void SaveProfile(ProfileViewModel model)
{
string filePath = Path.Combine(_env.ContentRootPath, "wwwroot/images/upload", model.FileName);
if (System.IO.File.Exists(model.FileName)) return;
System.IO.File.WriteAllBytes(filePath, model.File);
}

public class ProfileViewModel


{
public byte[] File { get; set; }
public string FileName { get; set; }
}

Ejemplo de enlazador de modelos personalizado


En esta sección implementaremos un enlazador de modelos personalizado que haga lo siguiente:
Convertir los datos de solicitud entrantes en argumentos clave fuertemente tipados
Usar Entity Framework Core para capturar la entidad asociada
Pasar la entidad asociada como argumento al método de acción
En el siguiente ejemplo se usa el atributo ModelBinder en el modelo Author :

using CustomModelBindingSample.Binders;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace CustomModelBindingSample.Data
{
[ModelBinder(BinderType = typeof(AuthorEntityBinder))]
public class Author
{
public int Id { get; set; }
public string Name { get; set; }
public string GitHub { get; set; }
public string Twitter { get; set; }
public string BlogUrl { get; set; }
}
}

En el código anterior, el atributo ModelBinder especifica el tipo de IModelBinder que se debe emplear para enlazar
parámetros de acción de Author .
AuthorEntityBinder sirve para enlazar un parámetro Author capturando la entidad de un origen de datos por
medio de Entity Framework Core y de un authorId :
public class AuthorEntityBinder : IModelBinder
{
private readonly AppDbContext _db;
public AuthorEntityBinder(AppDbContext db)
{
_db = db;
}

public Task BindModelAsync(ModelBindingContext bindingContext)


{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}

// Specify a default argument name if none is set by ModelBinderAttribute


var modelName = bindingContext.BinderModelName;
if (string.IsNullOrEmpty(modelName))
{
modelName = "authorId";
}

// Try to fetch the value of the argument by name


var valueProviderResult =
bindingContext.ValueProvider.GetValue(modelName);

if (valueProviderResult == ValueProviderResult.None)
{
return Task.CompletedTask;
}

bindingContext.ModelState.SetModelValue(modelName,
valueProviderResult);

var value = valueProviderResult.FirstValue;

// Check if the argument value is null or empty


if (string.IsNullOrEmpty(value))
{
return Task.CompletedTask;
}

int id = 0;
if (!int.TryParse(value, out id))
{
// Non-integer arguments result in model state errors
bindingContext.ModelState.TryAddModelError(
bindingContext.ModelName,
"Author Id must be an integer.");
return Task.CompletedTask;
}

// Model will be null if not found, including for


// out of range id values (0, -3, etc.)
var model = _db.Authors.Find(id);
bindingContext.Result = ModelBindingResult.Success(model);
return Task.CompletedTask;
}
}

En el siguiente código se indica cómo usar AuthorEntityBinder en un método de acción:


[HttpGet("get/{authorId}")]
public IActionResult Get(Author author)
{
return Ok(author);
}

El atributo ModelBinder se puede usar para aplicar AuthorEntityBinder a los parámetros que no usan
convenciones predeterminadas:

[HttpGet("{id}")]
public IActionResult GetById([ModelBinder(Name = "id")]Author author)
{
if (author == null)
{
return NotFound();
}
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
return Ok(author);
}

En este ejemplo, como el nombre del argumento no es el authorId predeterminado, se especifica en el parámetro
por medio del atributo ModelBinder . Observe que tanto el controlador como el método de acción se simplifican,
en contraste con tener que buscar la entidad en el método de acción. La lógica para capturar el autor a través de
Entity Framework Core se traslada al enlazador de modelos. Esto puede reducir enormemente la complejidad
cuando existen varios métodos que se enlazan con el modelo de autor, además de contribuir a seguir el principio
Una vez y solo una (DRY ).
Puede aplicar el atributo ModelBinder a propiedades de modelo individuales (como en un modelo de vista) o a
parámetros del método de acción para especificar un determinado nombre de modelo o enlazador de modelos
que sea exclusivo de ese tipo o acción en particular.
Implementar un ModelBinderProvider
En lugar de aplicar un atributo, puede implementar IModelBinderProvider . Así es como se implementan los
enlazadores de marco integrados. Cuando se especifica el tipo con el que el enlazador funciona, lo que se está
indicando es el tipo de argumento que ese enlazador genera, no la entrada que acepta. El siguiente proveedor de
enlazador funciona con AuthorEntityBinder . Cuando se agrega a la colección de proveedores de MVC, no es
necesario usar el atributo ModelBinder en los parámetros con tipo Author o Author .
using CustomModelBindingSample.Data;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using System;

namespace CustomModelBindingSample.Binders
{
public class AuthorEntityBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}

if (context.Metadata.ModelType == typeof(Author))
{
return new BinderTypeModelBinder(typeof(AuthorEntityBinder));
}

return null;
}
}
}

Nota: El código anterior devuelve un BinderTypeModelBinder . BinderTypeModelBinder actúa como una fábrica
de enlazadores de modelos y proporciona la inserción de dependencias. AuthorEntityBinder requiere que la
inserción de dependencias tenga acceso a Entity Framework Core. Use BinderTypeModelBinder si su enlazador
de modelos necesita servicios de inserción de dependencias.

Para usar un proveedor de enlazadores de modelos personalizado, agréguelo a ConfigureServices :

public void ConfigureServices(IServiceCollection services)


{
services.AddDbContext<AppDbContext>(options => options.UseInMemoryDatabase());

services.AddMvc(options =>
{
// add custom binder to beginning of collection
options.ModelBinderProviders.Insert(0, new AuthorEntityBinderProvider());
});
}

Al evaluar enlazadores de modelos, la colección de proveedores se examina en orden. Se usará el primer


proveedor que devuelva un enlazador.
En la siguiente imagen se muestran los enlazadores de modelos predeterminados en el depurador.
Si su proveedor se agrega al final de la colección, puede ocurrir que se llame a un enlazador de modelos integrado
antes que al suyo. En este ejemplo, el proveedor personalizado se agrega al principio de la colección para procurar
que se use en los argumentos de acción de Author .

public void ConfigureServices(IServiceCollection services)


{
services.AddDbContext<AppDbContext>(options => options.UseInMemoryDatabase());

services.AddMvc(options =>
{
// add custom binder to beginning of collection
options.ModelBinderProviders.Insert(0, new AuthorEntityBinderProvider());
});
}

Sugerencias y procedimientos recomendados


Los enlazadores de modelos personalizados deben caracterizarse por lo siguiente:
No deben tratar de establecer códigos de estado ni devolver resultados (por ejemplo, 404 No encontrado). Los
errores que se produzcan en un enlace de modelos se deben controlar con un filtro de acciones o con la lógica
del propio método de acción.
Son realmente útiles para eliminar el código repetitivo y las cuestiones transversales de los métodos de acción.
No se deben usar en general para convertir una cadena en un tipo personalizado. Para ello, TypeConverter
suele ser una mejor opción.
Compilación de API web con ASP.NET Core
21/06/2018 • 6 minutes to read • Edit Online

Por Scott Addie


Vea o descargue el código de ejemplo (cómo descargarlo)
En este documento se explica cómo crear una API web en ASP.NET Core y los casos en los que se recomienda
usar cada una.

Derivación de una clase desde ControllerBase


Herede desde la clase ControllerBase en un controlador diseñado para funcionar como API web. Por ejemplo:
[!code-csharp]
[!code-csharp]
La clase ControllerBase proporciona acceso a varios métodos y propiedades. En el ejemplo anterior, algunos
métodos de este tipo incluyen BadRequest y CreatedAtAction. Estos métodos se invocan en los métodos de
acción para devolver los códigos de estado HTTP 400 y 201, respectivamente. La propiedad ModelState, que
también se proporciona con ControllerBase , se usa para realizar la validación del modelo de solicitud.

Anotación de una clase con ApiControllerAttribute


ASP.NET Core 2.1 incorpora el atributo [ApiController] para designar una clase de controlador de API web. Por
ejemplo:
[!code-csharp]
Este atributo se suele emparejar con ControllerBase para acceder a propiedades y métodos útiles.
ControllerBase proporciona acceso a métodos como NotFound y File.

Otro enfoque consiste en crear una clase de controlador base personalizada anotada con el atributo
[ApiController] :

[!code-csharp]
En las secciones siguientes se describen las ventajas de las características que aporta el atributo.
Respuestas HTTP 400 automáticas
Los errores de validación desencadenan automáticamente una respuesta HTTP 400. El código siguiente deja de
ser necesario en las acciones:
[!code-csharp]
Este comportamiento predeterminado se deshabilita con siguiente código en Startup.ConfigureServices:
[!code-csharp]
Inferencia de parámetro de origen de enlace
Un atributo de origen de enlace define la ubicación del valor del parámetro de una acción. Existen los atributos de
origen de enlace siguientes:
ATRIBUTO ORIGEN DE ENLACE

[FromBody] Cuerpo de la solicitud

[FromForm] Datos del formulario en el cuerpo de la solicitud

[FromHeader] Encabezado de la solicitud

[FromQuery] Parámetro de la cadena de consulta de la solicitud

[FromRoute] Datos de ruta de la solicitud actual

[FromServices] Servicio de solicitud insertado como parámetro de acción

NOTE
No use [FromRoute] si los valores pueden contener %2f (es decir, / ) porque %2f no incluirá el carácter sin escape /
. Use [FromQuery] si el valor puede contener %2f .

Sin el [ApiController] atributo, los atributos de origen de enlace se definen explícitamente. En el ejemplo
siguiente, el atributo [FromQuery] indica que el valor del parámetro discontinuedOnly se proporciona en la
cadena de consulta de la dirección URL de la solicitud:
[!code-csharp]
Las reglas de inferencia se aplican para los orígenes de datos predeterminados de los parámetros de acción.
Estas reglas configuran los orígenes de enlace que aplicaría manualmente a los parámetros de acción. Los
atributos de origen de enlace presentan este comportamiento:
[FromBody] se infiere para los parámetros de tipo complejo. La excepción a esta regla es cualquier tipo
integrado complejo que tenga un significado especial, como IFormCollection o CancellationToken. El código
de inferencia del origen de enlace omite esos tipos especiales. Cuando una acción contiene más de un
parámetro que se especifica explícitamente (a través de [FromBody] ) o se infiere como enlazado desde el
cuerpo de la solicitud, se produce una excepción. Por ejemplo, las firmas de acción siguientes provocan una
excepción:
[!code-csharp]
[FromForm ] se infiere para los parámetros de acción de tipo IFormFile o IFormFileCollection. No se infiere
para los tipos simples o definidos por el usuario.
[FromRoute] se infiere para cualquier nombre de parámetro de acción que coincida con un parámetro de la
plantilla de ruta. Si varias rutas coinciden con un parámetro de acción, cualquier valor de ruta se considera
[FromRoute] .
[FromQuery] se infiere para cualquier otro parámetro de acción.

Las reglas de inferencia predeterminadas se deshabilitan con el código siguiente en Startup.ConfigureServices:


[!code-csharp]
Inferencia de solicitud de varios elementos o datos de formulario
Si un parámetro de acción se anota con el atributo [FromForm], se infiere el tipo de contenido de la solicitud
multipart/form-data .

El comportamiento predeterminado se deshabilita con el siguiente código en Startup.ConfigureServices:


[!code-csharp]
Requisito de enrutamiento mediante atributos
El enrutamiento mediante atributos pasa a ser un requisito. Por ejemplo:
[!code-csharp]
Las acciones dejan de estar disponibles a través de las rutas convencionales definidas en UseMvc o mediante
UseMvcWithDefaultRoute en Startup.Configure.

Recursos adicionales
Tipos de valor devuelto de acción del controlador
Formateadores personalizados
Aplicación de formato a datos de respuesta
Páginas de ayuda mediante Swagger
Enrutamiento a acciones del controlador
Temas avanzados sobre ASP.NET Core Web API
21/06/2018 • 2 minutes to read • Edit Online

Formateadores personalizados
Aplicación de formato a datos de respuesta
Probar, depurar y solucionar problemas en ASP.NET
Core
21/06/2018 • 2 minutes to read • Edit Online

Prueba
Prueba unitaria en .NET Core y .NET Standard
Vea cómo usar la prueba unitaria en proyectos de .NET Core y .NET Standard.
Pruebas de integración
Obtenga información sobre cómo las pruebas de integración garantizan que los componentes de una aplicación,
como la base de datos, el sistema de archivos y la red, funcionen correctamente en la infraestructura.
Pruebas unitarias de páginas de Razor
Descubra cómo crear pruebas unitarias para las aplicaciones de páginas de Razor.
Controladores de pruebas
Obtenga información sobre cómo probar la lógica del controlador en ASP.NET Core con Moq y xUnit.

Depuración
Depuración de código fuente de ASP.NET Core 2.x
Obtenga información sobre cómo depurar orígenes de .NET Core y ASP.NET Core.
Depuración remota
Descubra cómo configurar una aplicación ASP.NET Core en Visual Studio 2017, implementarla en IIS con Azure y
agregar el depurador remoto de Visual Studio.
Depuración de instantáneas
Obtenga información sobre cómo recopilar instantáneas sobre las excepciones más importantes con el fin de
tener los datos necesarios para diagnosticar problemas en la producción.

Solucionar problemas
Solucionar problemas
Conozca y solucione advertencias y errores en proyectos de ASP.NET Core.
Pruebas de integración de ASP.NET Core
22/06/2018 • 30 minutes to read • Edit Online

Por Luke Latham y Steve Smith


Pruebas de integración aseguran de que los componentes de una aplicación funcionen correctamente en un nivel
que incluye la infraestructura de soporte de la aplicación, como la base de datos, el sistema de archivos y la red.
ASP.NET Core es compatible con las pruebas de integración con un marco de pruebas unitarias con un host de
web de prueba y un servidor de prueba en memoria.
Este tema supone un conocimiento básico de las pruebas unitarias. Si familiarizado con conceptos de prueba,
consulte el pruebas unitarias en .NET Core y estándar de .NET tema y su contenido vinculado.
Vea o descargue el código de ejemplo (cómo descargarlo)
La aplicación de ejemplo es una aplicación de páginas de Razor y asume un conocimiento básico de las páginas
de Razor. Si no conoce las páginas de Razor, vea los temas siguientes:
Introducción a las páginas de Razor
Introducción a las páginas de Razor
Pruebas unitarias de páginas de Razor

Introducción a las pruebas de integración


Pruebas de integración evaluación componentes de una aplicación en un nivel más amplio que pruebas unitarias.
Pruebas unitarias se usan para probar los componentes de software independiente, como métodos de clase
individuales. Pruebas de integración confirmación que dos o más componentes de aplicación funcionan
conjuntamente para generar un resultado esperado, posiblemente incluyen todos los componentes necesarios
para procesar completamente una solicitud.
Estas pruebas más amplias se usan para probar la aplicación en la infraestructura y todo framework, a menudo
incluye los siguientes componentes:
Base de datos
Sistema de archivos
Dispositivos de red
Canalización de solicitudes y respuestas
Pruebas unitarias uso fabricado componentes, denominados fakes o simular objetos, en lugar de los
componentes de infraestructura.
A diferencia de pruebas unitarias, pruebas de integración:
Utilice los componentes reales que usa la aplicación en producción.
Se requieren más código y procesamiento de datos.
Tardar más tiempo en ejecutarse.
Por lo tanto, limite el uso de pruebas de integración para los escenarios más importantes de la infraestructura. Si
un comportamiento se puede probar con una prueba unitaria o una prueba de integración, elija la prueba
unitaria.
TIP
No escriba pruebas de integración para cada permutación posibles de acceso de archivos y datos con sistemas de archivos y
bases de datos. Independientemente de cuántos lugares a través de una aplicación interactuar con bases de datos y
sistemas de archivos, un conjunto de lectura, escritura, actualización y eliminación integración pruebas son normalmente
capaces de base de datos de prueba adecuadamente y componentes del sistema de archivos tiene el foco. Usar unidad de
pruebas para pruebas de rutina de la lógica del método que interactúan con estos componentes. En pruebas unitarias, el
uso de la infraestructura de fakes/simulacros resultado en una ejecución de pruebas más rápida.

NOTE
En las discusiones de pruebas de integración, el proyecto probado se suele denominar la sistema sometido a prueba, o
"SUT" para abreviar.

Pruebas de integración de ASP.NET Core


Pruebas de integración de ASP.NET Core requieren lo siguiente:
Un proyecto de prueba se usa para contener y ejecutar las pruebas. El proyecto de prueba tiene una referencia
al proyecto de ASP.NET Core probado, denominado el sistema sometido a prueba (SUT). "SUT" se utiliza a lo
largo de este tema para hacer referencia a la aplicación probada.
El proyecto de prueba crea un host de web de prueba para el SUT y usa a un cliente de servidor de prueba
para controlar las solicitudes y respuestas para el SUT.
Se utiliza un ejecutor de pruebas para ejecutar las pruebas y los informes de los resultados de pruebas.
Pruebas de integración siguen una secuencia de eventos que incluyen el habitual organizar, Act, y Assert pasos de
prueba:
1. Host de web de SUT está configurado.
2. Se crea un cliente de servidor de prueba para enviar solicitudes a la aplicación.
3. El organizar se ejecuta el paso de prueba: la aplicación de prueba prepara una solicitud.
4. El Act se ejecuta el paso de prueba: el cliente envía la solicitud y recibe la respuesta.
5. El Assert se ejecuta el paso de prueba: el real respuesta se valida como un pasar o producirá un error tomando
como base un esperado respuesta.
6. El proceso continúa hasta que todas las pruebas se ejecutan.
7. Se informa de los resultados de pruebas.
Normalmente, el host de prueba web está configurado de forma diferente de host de la aplicación web normal
para la prueba se ejecuta. Por ejemplo, podría utilizarse para las pruebas de otra base de datos o la configuración
de aplicación distinta.
Componentes de infraestructura, como el host de prueba web y el servidor de prueba en memoria (TestServer),
se proporciona o administrados por el Microsoft.AspNetCore.Mvc.Testing paquete. Uso de este paquete
simplifica la creación de la prueba y ejecución.
El Microsoft.AspNetCore.Mvc.Testing paquete controla las siguientes tareas:
Copia el archivo de dependencias (*.deps) desde el SUT en el proyecto de prueba bin carpeta.
Establece la raíz de contenido en la raíz del proyecto de SUT para que se encuentran archivos estáticos como
páginas o vistas cuando se ejecutan las pruebas.
Proporciona el WebApplicationFactory clase simplificar arrancar el SUT con TestServer .

El pruebas unitarias documentación describe cómo configurar un proyecto y probar el ejecutor de pruebas, así
como instrucciones detalladas sobre cómo ejecutar pruebas y recomendaciones sobre cómo para
comprobaciones de nombres y clases de prueba.

NOTE
Al crear un proyecto de prueba para una aplicación, separe las pruebas unitarias de las pruebas de integración en proyectos
diferentes. Esto ayuda a asegurarse de que accidentalmente componentes de infraestructura de pruebas no incluidos en las
pruebas unitarias. La separación de las pruebas de unidades y de integración también permite controlar en qué conjunto de
pruebas se ejecutan.

No hay prácticamente ninguna diferencia entre la configuración de pruebas de las aplicaciones de las páginas de
Razor y las aplicaciones MVC. La única diferencia está en cómo se denominan las pruebas. En una aplicación de
páginas de Razor, suelen denominarse pruebas de los puntos de conexión de la página después de la clase de
modelo de página (por ejemplo, IndexPageTests para probar la integración del componente de la página de
índice). En una aplicación MVC, pruebas normalmente están organizadas por las clases de controlador y los
controladores que se someten a prueba con el nombre (por ejemplo, HomeControllerTests para probar la
integración del componente para el controlador Home).

Requisitos previos de la aplicación de prueba


El proyecto de prueba debe:
Tiene una referencia de paquete para Microsoft.AspNetCore.App.
Use el SDK de Web en el archivo de proyecto ( <Project Sdk="Microsoft.NET.Sdk.Web"> ).

Estos prerequesities puede verse en la aplicación de ejemplo. Inspeccionar el


tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj archivo.

Pruebas básicas con el valor predeterminado WebApplicationFactory


WebApplicationFactory<TEntryPoint> se utiliza para crear un TestServer para las pruebas de integración.
TEntryPoint Normalmente es la clase de punto de entrada de la SUT la Startup clase.

Implementar clases de prueba un accesorio de clase interfaz ( IClassFixture ) para indicar la clase contiene
pruebas y proporciona instancias de objetos compartidos a través de las pruebas de la clase.
Prueba básica de extremos de la aplicación
La siguiente prueba (clase), BasicTests , usa el WebApplicationFactory para arrancar el SUT y proporcionar un
HttpClient a un método de prueba, Get_EndpointsReturnSuccessAndCorrectContentType . El método comprueba si el
código de estado de respuesta es correcto (códigos de estado en el intervalo de 200-299) y la Content-Type
encabezado es text/html; charset=utf-8 para varias páginas de la aplicación.
CreateClient crea una instancia de HttpClient que sigue redirecciones y controla las cookies automáticamente.
public class BasicTests
: IClassFixture<WebApplicationFactory<RazorPagesProject.Startup>>
{
private readonly WebApplicationFactory<RazorPagesProject.Startup> _factory;

public BasicTests(WebApplicationFactory<RazorPagesProject.Startup> factory)


{
_factory = factory;
}

[Theory]
[InlineData("/")]
[InlineData("/Index")]
[InlineData("/About")]
[InlineData("/Privacy")]
[InlineData("/Contact")]
public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
{
// Arrange
var client = _factory.CreateClient();

// Act
var response = await client.GetAsync(url);

// Assert
response.EnsureSuccessStatusCode(); // Status Code 200-299
Assert.Equal("text/html; charset=utf-8",
response.Content.Headers.ContentType.ToString());
}

Probar un extremo seguro


Otra prueba en el BasicTests clase comprueba que un punto de conexión seguro redirige un usuario no
autenticado a la página de inicio de sesión de la aplicación.
En el SUT el /SecurePage página usa un AuthorizePage convención para aplicar un AuthorizeFilter a la página.
Para obtener más información, consulte convenciones de autorización de las páginas de Razor.

services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
.AddRazorPagesOptions(options =>
{
options.Conventions.AuthorizePage("/SecurePage");
});

En el Get_SecurePageRequiresAnAuthenticatedUser probar, un WebApplicationFactoryClientOptions está


establecido en no permitir redirecciones estableciendo AllowAutoRedirect a false :
[Fact]
public async Task Get_SecurePageRequiresAnAuthenticatedUser()
{
// Arrange
var client = _factory.CreateClient(
new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});

// Act
var response = await client.GetAsync("/SecurePage");

// Assert
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.StartsWith("http://localhost/Identity/Account/Login",
response.Headers.Location.OriginalString);
}

Al no permitir el cliente sigue la redirección, se pueden realizar las siguientes comprobaciones:


Se puede comprobar el código de estado devuelto por el SUT con el esperado HttpStatusCode.Redirect
resultado, no el código de estado final después de la redirección a la página de inicio de sesión, que sería
HttpStatusCode.OK.
El Location se comprueba el valor del encabezado de los encabezados de respuesta para confirmar que inicia
con http://localhost/Identity/Account/Login , no el inicio de sesión página respuesta final, donde el Location
encabezado no estar presente.
Para obtener más información sobre WebApplicationFactoryClientOptions , consulte el opciones de cliente sección.

Personalizar WebApplicationFactory
Configuración del host Web puede crearse independientemente de las clases de prueba mediante la adquisición
de WebApplicationFactory para crear uno o varios generadores personalizados:
1. Heredar de WebApplicationFactory e invalide ConfigureWebHost. El IWebHostBuilder permite la
configuración de la colección de servicio con ConfigureServices:
public class CustomWebApplicationFactory<TStartup>
: WebApplicationFactory<RazorPagesProject.Startup>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
// Create a new service provider.
var serviceProvider = new ServiceCollection()
.AddEntityFrameworkInMemoryDatabase()
.BuildServiceProvider();

// Add a database context (ApplicationDbContext) using an in-memory


// database for testing.
services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseInMemoryDatabase("InMemoryDbForTesting");
options.UseInternalServiceProvider(serviceProvider);
});

// Build the service provider.


var sp = services.BuildServiceProvider();

// Create a scope to obtain a reference to the database


// context (ApplicationDbContext).
using (var scope = sp.CreateScope())
{
var scopedServices = scope.ServiceProvider;
var db = scopedServices.GetRequiredService<ApplicationDbContext>();
var logger = scopedServices
.GetRequiredService<ILogger<CustomWebApplicationFactory<TStartup>>>();

// Ensure the database is created.


db.Database.EnsureCreated();

try
{
// Seed the database with test data.
Utilities.InitializeDbForTests(db);
}
catch (Exception ex)
{
logger.LogError(ex, $"An error occurred seeding the " +
"database with test messages. Error: {ex.Message}");
}
}
});
}
}

En la propagación de la base de datos la aplicación de ejemplo realiza el InitializeDbForTests método. El


método se describe en el ejemplo de pruebas de integración: organización de la aplicación de prueba
sección.
2. Utilice la opción de instalación CustomWebApplicationFactory en las clases de prueba. En el ejemplo
siguiente se utiliza el generador en el IndexPageTests clase:
public class IndexPageTests : IClassFixture<CustomWebApplicationFactory<RazorPagesProject.Startup>>
{
private readonly HttpClient _client;
private readonly CustomWebApplicationFactory<RazorPagesProject.Startup> _factory;

public IndexPageTests(
CustomWebApplicationFactory<RazorPagesProject.Startup> factory)
{
_client = factory.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
_factory = factory;
}

Cliente de la aplicación de ejemplo está configurado para impedir la HttpClient de redirecciones


siguientes. Como se explica en la probar un extremo seguro sección, esto permite a pruebas para
comprobar el resultado de la primera respuesta de la aplicación. La primera respuesta es un
redireccionamiento en muchas de estas pruebas con un Location encabezado.
3. Utiliza una prueba normal el HttpClient y métodos auxiliares para procesar la solicitud y la respuesta:

[Fact]
public async Task Post_DeleteAllMessagesHandler_ReturnsRedirectToRoot()
{
// Arrange
var defaultPage = await _client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);

//Act
var response = await _client.SendAsync(
(IHtmlFormElement)content.QuerySelector("form[id='messages']"),
(IHtmlButtonElement)content.QuerySelector("button[id='deleteAllBtn']"));

// Assert
Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.Equal("/", response.Headers.Location.OriginalString);
}

Cualquier solicitud POST a la SUT debe cumplir la comprobación de antiforgery que se convierte
automáticamente en la aplicación antiforgery sistema de protección de datos. Para organizar de solicitud POST
de una prueba, la aplicación de prueba debe:
1. Realice una solicitud de la página.
2. Analizar la cookie antiforgery y el token de validación de solicitud de la respuesta.
3. Asegúrese de la solicitud POST con la validación de solicitud y cookie antiforgery símbolo (token) en su lugar.
El SendAsync métodos de extensión de aplicación auxiliar (Helpers/HttpClientExtensions.cs) y la GetDocumentAsync
método auxiliar (Helpers/HtmlHelpers.cs) en el deaplicacióndeejemplo usar la AngleSharp analizador para
controlar la comprobación de antiforgery con los métodos siguientes:
GetDocumentAsync – Recibe la HttpResponseMessage y devuelve un IHtmlDocument . GetDocumentAsync usa un
generador que prepara un respuesta virtual basado en el original HttpResponseMessage . Para obtener más
información, consulte el AngleSharp documentación.
SendAsync métodos de extensión para la HttpClient redactar una HttpRequestMessage y llame a
SendAsync(HttpRequestMessage) para enviar solicitudes a la SUT. Sobrecargas de SendAsync acepte el
formulario HTML ( IHtmlFormElement ) y los siguientes:
Botón del formulario de envío ( IHtmlElement )
Colección de valores de formulario ( IEnumerable<KeyValuePair<string, string>> )
Botón de envío ( IHtmlElement ) y valores del formulario ( IEnumerable<KeyValuePair<string, string>> )

NOTE
AngleSharp se utiliza con fines de demostración, en este tema y la aplicación de ejemplo de biblioteca de análisis de un
tercero. AngleSharp no es compatible o requerida para las pruebas de integración de aplicaciones de ASP.NET Core. Pueden
utilizar otros analizadores, como el Html agilidad Pack (GRACIA). Otro enfoque es escribir código para controlar el sistema
antiforgery token de comprobación de solicitud y cookie antiforgery directamente.

Personalizar al cliente con WithWebHostBuilder


Cuando se requiere un método de prueba, configuración adicional WithWebHostBuilder crea un nuevo
WebApplicationFactory con una IWebHostBuilder que más se personaliza mediante configuración.

El Post_DeleteMessageHandler_ReturnsRedirectToRoot probar el método de la aplicación de ejemplo muestra el uso


de WithWebHostBuilder . Esta prueba realiza una eliminación de registro en la base de datos mediante la activación
de un envío de formulario en el SUT.
Porque otra prueba en el IndexPageTests clase realiza una operación que eliminará todos los registros en la base
de datos y se puede ejecutar antes de la Post_DeleteMessageHandler_ReturnsRedirectToRoot (método), la base de
datos es visible en este método de prueba para asegurarse de que está presente para el SUT eliminar un registro.
Al seleccionar la deleteBtn1 botón de la messages se simula el formulario en el SUT en la solicitud a la SUT:
[Fact]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
var serviceProvider = services.BuildServiceProvider();

using (var scope = serviceProvider.CreateScope())


{
var scopedServices = scope.ServiceProvider;
var db = scopedServices
.GetRequiredService<ApplicationDbContext>();
var logger = scopedServices
.GetRequiredService<ILogger<IndexPageTests>>();

try
{
Utilities.InitializeDbForTests(db);
}
catch (Exception ex)
{
logger.LogError(ex, "An error occurred seeding " +
"the database with test messages. Error: " +
ex.Message);
}
}
});
})
.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
var defaultPage = await client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);

//Act
var response = await client.SendAsync(
(IHtmlFormElement)content.QuerySelector("form[id='messages']"),
(IHtmlButtonElement)content.QuerySelector("button[id='deleteBtn1']"));

// Assert
Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.Equal("/", response.Headers.Location.OriginalString);
}

Opciones de cliente
En la tabla siguiente se muestra el valor predeterminado WebApplicationFactoryClientOptions disponibles al
crear HttpClient instancias.

OPCIÓN DESCRIPCIÓN DEFAULT

AllowAutoRedirect Obtiene o establece si HttpClient true


instancias deben seguir
automáticamente las respuestas de
redirección.
OPCIÓN DESCRIPCIÓN DEFAULT

BaseAddress Obtiene o establece la dirección base http://localhost


del HttpClient instancias.

HandleCookies Obtiene o establece si HttpClient true


instancias deben controlar las cookies.

MaxAutomaticRedirections Obtiene o establece el número máximo 7


de respuestas de redirección que
HttpClient las instancias deben
seguir.

Crear el WebApplicationFactoryClientOptions clase y lo pasa a la CreateClient (método) (valor predeterminado en


el ejemplo de código se muestran los valores):

// Default client option values are shown


var clientOptions = new WebApplicationFactoryClientOptions();
clientOptions.AllowAutoRedirect = true;
clientOptions.BaseAddress = new Uri("http://localhost");
clientOptions.HandleCookies = true;
clientOptions.MaxAutomaticRedirections = 7;

_client = _factory.CreateClient(clientOptions);

Cómo la infraestructura de prueba deduce la ruta de acceso de


contenido raíz de aplicación
El WebApplicationFactory constructor deduce la ruta de acceso de contenido raíz de aplicación mediante la
búsqueda de un WebApplicationFactoryContentRootAttribute en el ensamblado que contiene las pruebas de
integración con una clave igual a la TEntryPoint ensamblado System.Reflection.Assembly.FullName . En caso de
que no se encuentra un atributo con la clave correcta, WebApplicationFactory vuelve a buscar un archivo de
solución (*.sln) y anexa el TEntryPoint nombre del ensamblado para el directorio de la solución. El directorio raíz
de aplicación (la ruta de acceso raíz del contenido) se usa para detectar las vistas y los archivos de contenido.
En la mayoría de los casos, no será necesario establecer explícitamente la raíz de contenido de la aplicación, como
la lógica de búsqueda normalmente busca la raíz de contenido correcta en tiempo de ejecución. En casos
especiales que no se encuentra la raíz de contenido mediante el algoritmo de búsqueda integradas, la aplicación
de contenido raíz se puede especificar explícitamente o mediante el uso de una lógica personalizada. Para
establecer la raíz de contenido de aplicación en esos escenarios, llame a la UseSolutionRelativeContentRoot
método de extensión de la Microsoft.AspNetCore.TestHost paquete. Proporcione la ruta de acceso relativa de la
solución y el patrón de nombre o glob del archivo de solución opcional (valor predeterminado = *.sln ).
Llame a la UseSolutionRelativeContentRoot método de extensión mediante una de los métodos siguientes:
Al configurar las clases de prueba con WebApplicationFactory , proporcione una configuración
personalizada con el IWebHostBuilder:
public IndexPageTests(
WebApplicationFactory<RazorPagesProject.Startup> factory)
{
var _factory = factory.WithWebHostBuilder(builder =>
{
builder.UseSolutionRelativeContentRoot("<SOLUTION-RELATIVE-PATH>");

...
});
}

Al configurar las clases de prueba con un personalizado WebApplicationFactory , heredar de


WebApplicationFactory e invalide ConfigureWebHost:

public class CustomWebApplicationFactory<TStartup>


: WebApplicationFactory<RazorPagesProject.Startup>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
builder.UseSolutionRelativeContentRoot("<SOLUTION-RELATIVE-PATH>");

...
});
}
}

Deshabilitar las copias sombra


Instantáneas hace que las pruebas se ejecutan en una carpeta diferente a la carpeta de salida. Para que las
pruebas para que funcione correctamente, debe deshabilitarse la operación de instantánea. El aplicación de
ejemplo utiliza xUnit y deshabilita la operación de copia sombra para xUnit mediante la inclusión de un
xunit.runner.json archivo con la opción de configuración correcto. Para obtener más información, consulte
configurar xUnit.net con JSON.
Agregar el xunit.runner.json archivo al nodo raíz del proyecto de prueba con el siguiente contenido:

{
"shadowCopy": false
}

Ejemplo de pruebas de integración


El aplicación de ejemplo se compone de dos aplicaciones:

APLICACIÓN CARPETA DE PROYECTO DESCRIPCIÓN

Aplicación de mensaje (el SUT) src/RazorPagesProject Permite a un usuario agregar, eliminar


uno, elimine todos y analizar los
mensajes.

Aplicación de prueba tests/RazorPagesProject.Tests Utilizado para la prueba de integración


de la SUT.

Se pueden ejecutar las pruebas con las características integradas de prueba de un IDE, como Visual Studio. Si usa
código de Visual Studio o la línea de comandos, ejecute el siguiente comando en un símbolo del sistema en el
tests/RazorPagesProject.Tests carpeta:

dotnet test

Organización de la aplicación (SUT ) de mensaje


El SUT es un sistema de mensajes de las páginas de Razor con las siguientes características:
La página de índice de la aplicación (Pages/Index.cshtml y Pages/Index.cshtml.cs) proporciona una interfaz de
usuario y la página métodos del modelo para controlar la adición, eliminación y análisis de mensajes (palabras
medios por mensaje) .
Describe un mensaje de la Message clase (Data/Message.cs) con dos propiedades: Id (clave) y Text
(mensaje). El Text propiedad es necesaria y se limita a 200 caracteres.
Los mensajes se almacenan con base de datos de Entity Framework en memoria†.
La aplicación contiene una capa de acceso a datos (DAL ) en su clase de contexto de base de datos,
AppDbContext ( Data/AppDbContext.cs).
Si la base de datos está vacía en el inicio de la aplicación, el almacén de mensajes se inicializa con tres
mensajes.
La aplicación incluye un /SecurePage que sólo pueda tener acceso un usuario autenticado.

†El tema EF prueba con InMemory, explica cómo utilizar una base de datos en memoria para las pruebas con
MSTest. Este tema se usa el xUnit marco de pruebas. Las implementaciones de prueba a través de los marcos de
pruebas diferente y conceptos de prueba son similares pero no idénticos.
Aunque la aplicación no usa el modelo de repositorio y no es un ejemplo efectivo de la patrón de la unidad de
trabajo (UoW ), las páginas de Razor admite estos patrones de desarrollo. Para obtener más información, consulte
diseñar la capa de persistencia de infraestructura, implementación del repositorio y patrones de unidad de trabajo
en una aplicación de MVC de ASP.NET, y controlador de pruebas lógica de (el ejemplo implementa el modelo de
repositorio).
Organización de la aplicación de prueba
La aplicación de prueba es una aplicación de consola en el tests/RazorPagesProject.Tests carpeta.

CARPETA DE LA APLICACIÓN DE PRUEBA DESCRIPCIÓN

BasicTests BasicTests.cs contiene métodos de prueba para el


enrutamiento, obtener acceso a una página segura por un
usuario no autenticado y obtener un perfil de usuario de
GitHub y comprobación de inicio de sesión de usuario del
perfil.

IntegrationTests IndexPageTests.cs contiene las pruebas de integración de la


página de índice mediante personalizado
WebApplicationFactory clase.

Aplicaciones auxiliares/utilidades Utilities.cs contiene el InitializeDbForTests


método utilizado para inicializar la base de datos con
datos de prueba.
HtmlHelpers.cs proporciona un método para devolver
un AngleSharp IHtmlDocument para su uso por los
métodos de prueba.
HttpClientExtensions.cs proporcionan sobrecargas
para SendAsync para enviar solicitudes a la SUT.
El marco de pruebas es xUnit. Pruebas de integración se realizan empleando el Microsoft.AspNetCore.TestHost,
que incluye el TestServer. Dado que la Microsoft.AspNetCore.Mvc.Testing paquete se utiliza para configurar el
servidor de prueba y el host de prueba, el TestHost y TestServer paquetes no requieren referencias del paquete
directa en el archivo de proyecto de la aplicación de prueba o configuración de desarrollador de la aplicación de
prueba.
La propagación de la base de datos para las pruebas
Pruebas de integración suelen requieran un pequeño conjunto de datos en la base de datos antes de la ejecución
de prueba. Por ejemplo, una eliminación pruebas llamadas en busca de una eliminación de registros de base de
datos, de modo que la base de datos debe tener al menos un registro para la solicitud de eliminación tenga éxito.
Valores de inicialización de la aplicación de ejemplo con tres mensajes en la base de datos Utilities.cs que las
pruebas se pueden usar cuando ejecuta:

public static void InitializeDbForTests(ApplicationDbContext db)


{
db.Messages.AddRange(GetSeedingMessages());
db.SaveChanges();
}

public static List<Message> GetSeedingMessages()


{
return new List<Message>()
{
new Message(){ Text = "TEST RECORD: You're standing on my scarf." },
new Message(){ Text = "TEST RECORD: Would you like a jelly baby?" },
new Message(){ Text = "TEST RECORD: To the rational mind, " +
"nothing is inexplicable; only unexplained." }
};
}

Recursos adicionales
Pruebas unitarias
Pruebas unitarias de páginas de Razor
Middleware
Controladores de pruebas
Pruebas de unidad de páginas de Razor en ASP.NET
Core
22/06/2018 • 18 minutes to read • Edit Online

Por Luke Latham


ASP.NET Core es compatible con las pruebas unitarias de aplicaciones de las páginas de Razor. Pruebas de los
datos tienen acceso a la capa (DAL ) y asegurarse de modelos de página:
Partes de una aplicación de páginas de Razor funcionan juntos como una unidad y de manera independiente
durante la construcción de la aplicación.
Clases y métodos han limitado ámbitos de responsabilidad.
Documentación adicional existe en el comportamiento de la aplicación.
Las regresiones, que son errores imperiosa actualizaciones en el código, se encuentran durante la
implementación y compilación automatizada.
En este tema se da por supuesto que tiene un conocimiento básico de aplicaciones de las páginas de Razor y
pruebas unitarias. Si no está familiarizado con conceptos de prueba o de aplicaciones de las páginas de Razor, vea
los temas siguientes:
Introducción a las páginas de Razor
Introducción a las páginas de Razor
Pruebas unitarias de C# en .NET Core con xUnit y prueba de dotnet.
Vea o descargue el código de ejemplo (cómo descargarlo)
El proyecto de ejemplo se compone de dos aplicaciones:

APLICACIÓN CARPETA DE PROYECTO DESCRIPCIÓN

Aplicación de mensaje src/RazorPagesTestSample Permite a un usuario agregar, eliminar


uno, elimine todos y analizar los
mensajes.

Aplicación de prueba tests/RazorPagesTestSample.Tests Utilizado para la prueba unitaria de la


aplicación de mensaje: acceso a datos
(DAL) de la capa y el modelo de páginas
de índice.

Se pueden ejecutar las pruebas con las características integradas de prueba de un IDE, como Visual Studio. Si usa
código de Visual Studio o la línea de comandos, ejecute el siguiente comando en un símbolo del sistema en el
tests/RazorPagesTestSample.Tests carpeta:

dotnet test

Organización de la aplicación de mensaje


La aplicación de mensaje es un sistema de mensajes de las páginas de Razor simple con las siguientes
características:
La página de índice de la aplicación (Pages/Index.cshtml y Pages/Index.cshtml.cs) proporciona una interfaz de
usuario y la página métodos del modelo para controlar la adición, eliminación y análisis de mensajes (palabras
medios por mensaje) .
Describe un mensaje de la Message clase (Data/Message.cs) con dos propiedades: Id (clave) y Text
(mensaje). El Text propiedad es necesaria y se limita a 200 caracteres.
Los mensajes se almacenan con base de datos de Entity Framework en memoria†.
La aplicación contiene una capa de acceso a datos (DAL ) en su clase de contexto de base de datos,
AppDbContext ( Data/AppDbContext.cs). Los métodos de la capa DAL se marcan virtual , lo que permite a los
métodos para su uso en las pruebas de simulación.
Si la base de datos está vacía en el inicio de la aplicación, el almacén de mensajes se inicializa con tres
mensajes. Estos propagado mensajes también se utilizan en pruebas.
†El tema EF prueba con InMemory, explica cómo utilizar una base de datos en memoria para las pruebas con
MSTest. Este tema se usa el xUnit marco de pruebas. Las implementaciones de prueba a través de los marcos de
pruebas diferente y conceptos de prueba son similares pero no idénticos.
Aunque la aplicación no usa el modelo de repositorio y no es un ejemplo efectivo de la patrón de la unidad de
trabajo (UoW ), las páginas de Razor admite estos patrones de desarrollo. Para obtener más información, consulte
diseñar la capa de persistencia de infraestructura, implementación del repositorio y patrones de unidad de trabajo
en una aplicación de MVC de ASP.NET, y controlador de pruebas lógica de (el ejemplo implementa el modelo de
repositorio).

Organización de la aplicación de prueba


La aplicación de prueba es una aplicación de consola en el tests/RazorPagesTestSample.Tests carpeta.

CARPETA DE LA APLICACIÓN DE PRUEBA DESCRIPCIÓN

UnitTests DataAccessLayerTest.cs contiene las pruebas unitarias


para la capa DAL.
IndexPageTests.cs contiene las pruebas unitarias para
el modelo de páginas de índice.

Utilidades Contiene el TestingDbContextOptions método usado para


crear nueva base de datos de las opciones de contexto para
cada prueba unitaria DAL para que la base de datos se
restablece a su estado de línea de base para cada prueba.

El marco de pruebas es xUnit. El objeto de marco de simulación es Moq.

Pruebas de unidad de los datos tienen acceso a la capa (DAL)


La aplicación de mensaje tiene un DAL con cuatro métodos incluidos en el AppDbContext clase
(src/RazorPagesTestSample/Data/AppDbContext.cs). Cada método tiene una o dos pruebas unitarias de la
aplicación de prueba.

MÉTODO DAL FUNCIÓN

GetMessagesAsync Obtiene un List<Message> desde la base de datos


ordenado por la Text propiedad.

AddMessageAsync Agrega un Message a la base de datos.


MÉTODO DAL FUNCIÓN

DeleteAllMessagesAsync Todos los elimina Message las entradas de la base de datos.

DeleteMessageAsync Elimina una sola Message desde la base de datos Id .

Pruebas unitarias de la capa DAL requieren DbContextOptions al crear un nuevo AppDbContext para cada prueba.
Un enfoque para crear el DbContextOptions para cada prueba es usar un DbContextOptionsBuilder:

var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>()


.UseInMemoryDatabase("InMemoryDb");

using (var db = new AppDbContext(optionsBuilder.Options))


{
// Use the db here in the unit test.
}

El problema con este enfoque es que cada prueba recibe la base de datos en el estado de la prueba anterior dejó.
Esto puede ser problemático al intentar escribir pruebas de unidad atómica que no interfieren entre sí. Para forzar
la AppDbContext para utilizar un nuevo contexto de base de datos para cada prueba, proporcione un
DbContextOptions instancia que se basa en un nuevo proveedor de servicios. La aplicación de prueba muestra
cómo hacer esto con su Utilities método de la clase TestingDbContextOptions
(tests/RazorPagesTestSample.Tests/Utilities/Utilities.cs):

public static DbContextOptions<AppDbContext> TestDbContextOptions()


{
// Create a new service provider to create a new in-memory database.
var serviceProvider = new ServiceCollection()
.AddEntityFrameworkInMemoryDatabase()
.BuildServiceProvider();

// Create a new options instance using an in-memory database and


// IServiceProvider that the context should resolve all of its
// services from.
var builder = new DbContextOptionsBuilder<AppDbContext>()
.UseInMemoryDatabase("InMemoryDb")
.UseInternalServiceProvider(serviceProvider);

return builder.Options;
}

Mediante el DbContextOptions en la unidad de la capa DAL pruebas permite que cada prueba para ejecutarse de
forma atómica con una instancia nueva de la base de datos:

using (var db = new AppDbContext(Utilities.TestingDbContextOptions()))


{
// Use the db here in the unit test.
}

Cada método de prueba en el DataAccessLayerTest clase (UnitTests/DataAccessLayerTest.cs) sigue un patrón


similar Assert para organizar Act:
1. Organizar: La base de datos está configurado para la prueba o se define el resultado esperado.
2. Acción: En la que se ejecuta la prueba.
3. Aserción: Las aserciones se realizan para determinar si el resultado de la prueba es correcta.
Por ejemplo, el DeleteMessageAsync método es responsable de quitar un único mensaje identificado por su Id
(src/RazorPagesTestSample/Data/AppDbContext.cs):

public async virtual Task DeleteMessageAsync(int id)


{
var message = await Messages.FindAsync(id);

if (message != null)
{
Messages.Remove(message);
await SaveChangesAsync();
}
}

Hay dos pruebas para este método. Una prueba comprueba que el método elimina un mensaje cuando el mensaje
está presente en la base de datos. Las demás pruebas de método que la base de datos no cambia si el mensaje
Id para su eliminación no existe. El DeleteMessageAsync_MessageIsDeleted_WhenMessageIsFound método se muestra
a continuación:

[Fact]
public async Task DeleteMessageAsync_MessageIsDeleted_WhenMessageIsFound()
{
using (var db = new AppDbContext(Utilities.TestDbContextOptions()))
{
// Arrange
var seedMessages = AppDbContext.GetSeedingMessages();
await db.AddRangeAsync(seedMessages);
await db.SaveChangesAsync();
var recId = 1;
var expectedMessages =
seedMessages.Where(message => message.Id != recId).ToList();

// Act
await db.DeleteMessageAsync(recId);

// Assert
var actualMessages = await db.Messages.AsNoTracking().ToListAsync();
Assert.Equal(
expectedMessages.OrderBy(x => x.Id),
actualMessages.OrderBy(x => x.Id),
new Utilities.MessageComparer());
}
}

En primer lugar, el método realiza el paso de organización, donde realiza la preparación para el paso de acción.
Los mensajes de propagación se obtienen y mantienen en seedMessages . Los mensajes de propagación se
guardan en la base de datos. El mensaje con un Id de 1 está establecido para su eliminación. Cuando el
DeleteMessageAsync método se ejecuta, los mensajes esperados deben tener todos los mensajes excepto el con
una Id de 1 . El expectedMessages variable representa este resultado esperado.

// Arrange
var seedMessages = AppDbContext.GetSeedingMessages();
await db.AddRangeAsync(seedMessages);
await db.SaveChangesAsync();
var recId = 1;
var expectedMessages =
seedMessages.Where(message => message.Id != recId).ToList();

El método actúa: el DeleteMessageAsync método se ejecuta pasando el recId de 1 :


// Act
await db.DeleteMessageAsync(recId);

Por último, el método obtiene la Messages desde el contexto y lo compara con el expectedMessages validar que los
dos son iguales:

// Assert
var actualMessages = await db.Messages.AsNoTracking().ToListAsync();
Assert.Equal(
expectedMessages.OrderBy(m => m.Id).Select(m => m.Text),
actualMessages.OrderBy(m => m.Id).Select(m => m.Text));

Para que compare los dos List<Message> son los mismos:


Los mensajes se ordenan por Id .
Se comparan los pares de mensajes en el Text propiedad.

Un método de prueba similar, DeleteMessageAsync_NoMessageIsDeleted_WhenMessageIsNotFound comprueba el


resultado de intentar eliminar un mensaje que no existe. En este caso, los mensajes esperados en la base de datos
deben ser iguales a los mensajes reales después de la DeleteMessageAsync se ejecuta el método. No debería haber
ningún cambio en el contenido de la base de datos:

[Fact]
public async Task DeleteMessageAsync_NoMessageIsDeleted_WhenMessageIsNotFound()
{
using (var db = new AppDbContext(Utilities.TestDbContextOptions()))
{
// Arrange
var expectedMessages = AppDbContext.GetSeedingMessages();
await db.AddRangeAsync(expectedMessages);
await db.SaveChangesAsync();
var recId = 4;

// Act
await db.DeleteMessageAsync(recId);

// Assert
var actualMessages = await db.Messages.AsNoTracking().ToListAsync();
Assert.Equal(
expectedMessages.OrderBy(m => m.Id).Select(m => m.Text),
actualMessages.OrderBy(m => m.Id).Select(m => m.Text));
}
}

Pruebas unitarias de los métodos del modelo de página


Otro conjunto de pruebas unitarias es responsable de pruebas de métodos del modelo de página. En la aplicación
de mensaje, los modelos de página de índice se encuentran en el IndexModel clase
src/RazorPagesTestSample/Pages/Index.cshtml.cs.

MÉTODO DEL MODELO DE PÁGINA FUNCIÓN

OnGetAsync Obtiene los mensajes de la capa DAL para la interfaz de


usuario con el GetMessagesAsync método.
MÉTODO DEL MODELO DE PÁGINA FUNCIÓN

OnPostAddMessageAsync Si el ModelState es válido, las llamadas AddMessageAsync


para agregar un mensaje a la base de datos.

OnPostDeleteAllMessagesAsync Llamadas DeleteAllMessagesAsync para eliminar todos los


mensajes en la base de datos.

OnPostDeleteMessageAsync Ejecuta DeleteMessageAsync para eliminar un mensaje con


el Id especificado.

OnPostAnalyzeMessagesAsync Si uno o más mensajes en la base de datos, calcula el número


medio de palabras por mensaje.

Los métodos del modelo de página se prueban utilizando siete pruebas en la IndexPageTests clase
(tests/RazorPagesTestSample.Tests/UnitTests/IndexPageTests.cs). Las pruebas usan el patrón de Act Assert
organizar familiarizado. Estas pruebas se centran en:
Determinar si los métodos siguen el comportamiento correcto cuando el ModelState no es válido.
Confirmar los métodos generan el valor correcto IActionResult .
Comprobando que las asignaciones de valor de propiedad se realizan correctamente.
Este grupo de pruebas a menudo simular los métodos de la capa DAL para generar datos esperado para el paso
de acción que se ejecuta un método de modelo de página. Por ejemplo, el GetMessagesAsync método de la
AppDbContext simulada para generar un resultado. Cuando un método de modelo de la página ejecuta este
método, el simulacro devuelve el resultado. Los datos no proceden de la base de datos. Esto crea condiciones de
prueba predecible y confiable para el uso de la capa DAL en las pruebas de modelo de página.
El OnGetAsync_PopulatesThePageModel_WithAListOfMessages prueba muestra cómo el GetMessagesAsync método es
simulado para el modelo de páginas:

var mockAppDbContext = new Mock<AppDbContext>(optionsBuilder.Options);


var expectedMessages = AppDbContext.GetSeedingMessages();
mockAppDbContext.Setup(
db => db.GetMessagesAsync()).Returns(Task.FromResult(expectedMessages));
var pageModel = new IndexModel(mockAppDbContext.Object);

Cuando el OnGetAsync método se ejecuta en el paso de acción, llama al modelo de página GetMessagesAsync
método.
Paso de acción de prueba unitaria (tests/RazorPagesTestSample.Tests/UnitTests/IndexPageTests.cs):

// Act
await pageModel.OnGetAsync();

IndexPage modelo de páginas OnGetAsync método (src/RazorPagesTestSample/Pages/Index.cshtml.cs):

public async Task OnGetAsync()


{
Messages = await _db.GetMessagesAsync();
}

El GetMessagesAsync método en la capa DAL no devuelve el resultado de llamar a este método. La versión del
método simulada devuelve el resultado.
En el Assert paso, los mensajes reales ( actualMessages ) se asignan a partir del Messages propiedad del modelo
de página. También se realiza una comprobación de tipo cuando se asignen los mensajes. Los mensajes esperados
y reales se comparan sus Text propiedades. La prueba valida que los dos List<Message> instancias contienen los
mismos mensajes.

// Assert
var actualMessages = Assert.IsAssignableFrom<List<Message>>(pageModel.Messages);
Assert.Equal(
expectedMessages.OrderBy(m => m.Id).Select(m => m.Text),
actualMessages.OrderBy(m => m.Id).Select(m => m.Text));

Otras pruebas en este grupo crean página de objetos del modelo que incluyen la DefaultHttpContext , el
ModelStateDictionary , ActionContext para establecer el PageContext , un ViewDataDictionary y un PageContext .
Estos son útiles para realizar las pruebas. Por ejemplo, la aplicación de mensaje establece una ModelState error
con AddModelError para comprobar que válido PageResult se devuelve cuando OnPostAddMessageAsync se ejecuta:

[Fact]
public async Task OnPostAddMessageAsync_ReturnsAPageResult_WhenModelStateIsInvalid()
{
// Arrange
var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>()
.UseInMemoryDatabase("InMemoryDb");
var mockAppDbContext = new Mock<AppDbContext>(optionsBuilder.Options);
var expectedMessages = AppDbContext.GetSeedingMessages();
mockAppDbContext.Setup(db => db.GetMessagesAsync()).Returns(Task.FromResult(expectedMessages));
var httpContext = new DefaultHttpContext();
var modelState = new ModelStateDictionary();
var actionContext = new ActionContext(httpContext, new RouteData(), new PageActionDescriptor(),
modelState);
var modelMetadataProvider = new EmptyModelMetadataProvider();
var viewData = new ViewDataDictionary(modelMetadataProvider, modelState);
var tempData = new TempDataDictionary(httpContext, Mock.Of<ITempDataProvider>());
var pageContext = new PageContext(actionContext)
{
ViewData = viewData
};
var pageModel = new IndexModel(mockAppDbContext.Object)
{
PageContext = pageContext,
TempData = tempData,
Url = new UrlHelper(actionContext)
};
pageModel.ModelState.AddModelError("Message.Text", "The Text field is required.");

// Act
var result = await pageModel.OnPostAddMessageAsync();

// Assert
Assert.IsType<PageResult>(result);
}

Recursos adicionales
Pruebas unitarias de C# en .NET Core con xUnit y prueba de dotnet.
Controladores de pruebas
El código de prueba unitaria (Visual Studio)
Pruebas de integración
xUnit.net
Introducción a xUnit.net (.NET Core/ASP.NET Core)
Moq
Inicio rápido de Moq
Probar la lógica del controlador en ASP.NET Core
25/06/2018 • 27 minutes to read • Edit Online

Por Steve Smith


Los controladores de las aplicaciones ASP.NET MVC deben ser de tamaño reducido e ir enfocados a abordar
problemas de la interfaz de usuario. Los controladores de gran tamaño que tratan problemas no relacionados
con la interfaz de usuario son bastante más difíciles de probar y mantener.
Ver o descargar el ejemplo desde GitHub

Probar los controladores


Los controladores son una parte fundamental de cualquier aplicación ASP.NET Core MVC. Por tanto, debe
tener la seguridad de que se comportan según lo previsto en la aplicación. Las pruebas automatizadas pueden
darle esta seguridad, así como detectar errores antes de que lleguen a la fase producción. Es importante no
asignar responsabilidades innecesarias a los controladores y procurar que las pruebas se centran únicamente en
las responsabilidades del controlador.
La lógica de controlador debería ser mínima y no ir enfocada a cuestiones de infraestructura o lógica
empresarial (por ejemplo, el acceso a datos). Compruebe la lógica del controlador, no el marco. Compruebe el
comportamiento del controlador en función de las entradas válidas o no válidas. Compruebe las respuestas de
controlador según el resultado de la operación empresarial que realiza.
Estas son algunas de las responsabilidades habituales de los controladores:
Comprobar ModelState.IsValid
Devolver una respuesta de error si ModelState no es válido
Recuperar una entidad de negocio de la persistencia
Llevar a cabo una acción en la entidad empresarial
Guardar la entidad comercial para persistencia
Devolver un IActionResult apropiado

Pruebas unitarias
Las pruebas unitarias conllevan probar una parte de una aplicación de forma aislada con respecto a su
infraestructura y dependencias. Cuando se realizan pruebas unitarias de la lógica de controlador, solo se
comprueba el contenido de una única acción, no el comportamiento de sus dependencias o del marco en sí.
Cuando realice pruebas unitarias de sus acciones de controlador, asegúrese de que solo se centran en el
comportamiento. Una prueba unitaria de controlador evita tener que recurrir a elementos como los filtros, el
enrutamiento o el enlace de modelos. Al centrarse en comprobar solo una cosa, las pruebas unitarias suelen ser
fáciles de escribir y rápidas de ejecutar. Un conjunto de pruebas unitarias bien escrito se puede ejecutar con
frecuencia sin demasiada sobrecarga. Pero las pruebas unitarias no detectan problemas de interacción entre
componentes, que es el propósito de las pruebas de integración.
Si está escribiendo filtros personalizados, rutas, etc., debería realizar pruebas unitarias en ellos, pero no como
parte de las comprobaciones de una acción de controlador determinada, sino de forma aislada.
TIP
Cree y ejecute pruebas unitarias con Visual Studio.

Para explicar las pruebas unitarias, revisaremos el siguiente controlador. Muestra una lista de sesiones de lluvia
de ideas y permite crear nuevas sesiones de lluvia de ideas con un método POST:
using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using TestingControllersSample.Core.Interfaces;
using TestingControllersSample.Core.Model;
using TestingControllersSample.ViewModels;

namespace TestingControllersSample.Controllers
{
public class HomeController : Controller
{
private readonly IBrainstormSessionRepository _sessionRepository;

public HomeController(IBrainstormSessionRepository sessionRepository)


{
_sessionRepository = sessionRepository;
}

public async Task<IActionResult> Index()


{
var sessionList = await _sessionRepository.ListAsync();

var model = sessionList.Select(session => new StormSessionViewModel()


{
Id = session.Id,
DateCreated = session.DateCreated,
Name = session.Name,
IdeaCount = session.Ideas.Count
});

return View(model);
}

public class NewSessionModel


{
[Required]
public string SessionName { get; set; }
}

[HttpPost]
public async Task<IActionResult> Index(NewSessionModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
else
{
await _sessionRepository.AddAsync(new BrainstormSession()
{
DateCreated = DateTimeOffset.Now,
Name = model.SessionName
});
}

return RedirectToAction(actionName: nameof(Index));


}
}
}

El controlador sigue el principio de dependencias explícitas, de modo que espera que la inserción de
dependencias le proporcione una instancia de IBrainstormSessionRepository . Esto es bastante sencillo de
comprobar si se usa un marco de objeto ficticio, como Moq. El método HTTP GET Index no tiene bucles ni
bifurcaciones y solamente llama a un método. Para probar este método Index , tenemos que confirmar que se
devuelve un ViewResult , con un ViewModel del método List del repositorio.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Moq;
using TestingControllersSample.Controllers;
using TestingControllersSample.Core.Interfaces;
using TestingControllersSample.Core.Model;
using TestingControllersSample.ViewModels;
using Xunit;

namespace TestingControllersSample.Tests.UnitTests
{
public class HomeControllerTests
{
[Fact]
public async Task Index_ReturnsAViewResult_WithAListOfBrainstormSessions()
{
// Arrange
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.ListAsync()).Returns(Task.FromResult(GetTestSessions()));
var controller = new HomeController(mockRepo.Object);

// Act
var result = await controller.Index();

// Assert
var viewResult = Assert.IsType<ViewResult>(result);
var model = Assert.IsAssignableFrom<IEnumerable<StormSessionViewModel>>(
viewResult.ViewData.Model);
Assert.Equal(2, model.Count());
}

private List<BrainstormSession> GetTestSessions()


{
var sessions = new List<BrainstormSession>();
sessions.Add(new BrainstormSession()
{
DateCreated = new DateTime(2016, 7, 2),
Id = 1,
Name = "Test One"
});
sessions.Add(new BrainstormSession()
{
DateCreated = new DateTime(2016, 7, 1),
Id = 2,
Name = "Test Two"
});
return sessions;
}
}
}

El método HomeController HTTP POST Index (mostrado arriba) debe comprobar lo siguiente:
El método de acción devuelve un ViewResult de solicitud incorrecta con los datos adecuados cuando
ModelState.IsValid es false .

Se llama al método Add en el repositorio y se devuelve un RedirectToActionResult con los argumentos


correctos cuando ModelState.IsValid es true.
El estado de modelo no válido se puede comprobar introduciendo errores con AddModelError , como se muestra
en la primera prueba de abajo.

[Fact]
public async Task IndexPost_ReturnsBadRequestResult_WhenModelStateIsInvalid()
{
// Arrange
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.ListAsync()).Returns(Task.FromResult(GetTestSessions()));
var controller = new HomeController(mockRepo.Object);
controller.ModelState.AddModelError("SessionName", "Required");
var newSession = new HomeController.NewSessionModel();

// Act
var result = await controller.Index(newSession);

// Assert
var badRequestResult = Assert.IsType<BadRequestObjectResult>(result);
Assert.IsType<SerializableError>(badRequestResult.Value);
}

[Fact]
public async Task IndexPost_ReturnsARedirectAndAddsSession_WhenModelStateIsValid()
{
// Arrange
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.AddAsync(It.IsAny<BrainstormSession>()))
.Returns(Task.CompletedTask)
.Verifiable();
var controller = new HomeController(mockRepo.Object);
var newSession = new HomeController.NewSessionModel()
{
SessionName = "Test Name"
};

// Act
var result = await controller.Index(newSession);

// Assert
var redirectToActionResult = Assert.IsType<RedirectToActionResult>(result);
Assert.Null(redirectToActionResult.ControllerName);
Assert.Equal("Index", redirectToActionResult.ActionName);
mockRepo.Verify();
}

La primera prueba confirma cuándo ModelState no es válido; se devuelve el mismo ViewResult que para una
solicitud GET . Cabe decir que la prueba no intenta pasar un modelo no válido. Eso no funcionaría de todas
formas, ya que el enlace de modelos no se está ejecutando (aunque una prueba de integración sí usaría el
enlace de modelos). En este caso concreto no estamos comprobando el enlace de modelos. Con estas pruebas
unitarias solamente estamos comprobando lo que el código del método de acción hace.
La segunda prueba comprueba si, cuando ModelState es válido, se agrega un nuevo BrainstormSession (a
través del repositorio) y el método devuelve un RedirectToActionResult con las propiedades que se esperan.
Las llamadas ficticias que no se efectúan se suelen omitir, aunque llamar a Verifiable al final de la llamada nos
permite confirmar esto en la prueba. Esto se logra con una llamada a mockRepo.Verify , que producirá un error
en la prueba si no se ha llamado al método esperado.
NOTE
La biblioteca Moq usada en este ejemplo nos permite mezclar fácilmente objetos ficticios comprobables (o "estrictos") con
objetos ficticios no comprobables (también denominados "flexibles" o stub). Obtenga más información sobre cómo
personalizar el comportamiento de objetos ficticios con Moq.

Otro controlador de la aplicación muestra información relacionada con una sesión de lluvia de ideas
determinada. Este controlador incluye lógica para tratar los valores de identificador no válidos:

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using TestingControllersSample.Core.Interfaces;
using TestingControllersSample.ViewModels;

namespace TestingControllersSample.Controllers
{
public class SessionController : Controller
{
private readonly IBrainstormSessionRepository _sessionRepository;

public SessionController(IBrainstormSessionRepository sessionRepository)


{
_sessionRepository = sessionRepository;
}

public async Task<IActionResult> Index(int? id)


{
if (!id.HasValue)
{
return RedirectToAction(actionName: nameof(Index), controllerName: "Home");
}

var session = await _sessionRepository.GetByIdAsync(id.Value);


if (session == null)
{
return Content("Session not found.");
}

var viewModel = new StormSessionViewModel()


{
DateCreated = session.DateCreated,
Name = session.Name,
Id = session.Id
};

return View(viewModel);
}
}
}

La acción de controlador tiene tres casos que comprobar, uno por cada instrucción return :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Moq;
using TestingControllersSample.Controllers;
using TestingControllersSample.Core.Interfaces;
using TestingControllersSample.Core.Model;
using TestingControllersSample.ViewModels;
using Xunit;
namespace TestingControllersSample.Tests.UnitTests
{
public class SessionControllerTests
{
[Fact]
public async Task IndexReturnsARedirectToIndexHomeWhenIdIsNull()
{
// Arrange
var controller = new SessionController(sessionRepository: null);

// Act
var result = await controller.Index(id: null);

// Assert
var redirectToActionResult = Assert.IsType<RedirectToActionResult>(result);
Assert.Equal("Home", redirectToActionResult.ControllerName);
Assert.Equal("Index", redirectToActionResult.ActionName);
}

[Fact]
public async Task IndexReturnsContentWithSessionNotFoundWhenSessionNotFound()
{
// Arrange
int testSessionId = 1;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.Returns(Task.FromResult((BrainstormSession)null));
var controller = new SessionController(mockRepo.Object);

// Act
var result = await controller.Index(testSessionId);

// Assert
var contentResult = Assert.IsType<ContentResult>(result);
Assert.Equal("Session not found.", contentResult.Content);
}

[Fact]
public async Task IndexReturnsViewResultWithStormSessionViewModel()
{
// Arrange
int testSessionId = 1;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.Returns(Task.FromResult(GetTestSessions().FirstOrDefault(s => s.Id == testSessionId)));
var controller = new SessionController(mockRepo.Object);

// Act
var result = await controller.Index(testSessionId);

// Assert
var viewResult = Assert.IsType<ViewResult>(result);
var model = Assert.IsType<StormSessionViewModel>(viewResult.ViewData.Model);
Assert.Equal("Test One", model.Name);
Assert.Equal(2, model.DateCreated.Day);
Assert.Equal(testSessionId, model.Id);
}

private List<BrainstormSession> GetTestSessions()


{
var sessions = new List<BrainstormSession>();
sessions.Add(new BrainstormSession()
{
DateCreated = new DateTime(2016, 7, 2),
Id = 1,
Name = "Test One"
});
sessions.Add(new BrainstormSession()
{
DateCreated = new DateTime(2016, 7, 1),
Id = 2,
Name = "Test Two"
});
return sessions;
}
}
}

La aplicación expone la funcionalidad como una API web (una lista de ideas asociadas a una sesión de lluvia de
ideas y un método para agregar nuevas ideas a una sesión):

using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using TestingControllersSample.ClientModels;
using TestingControllersSample.Core.Interfaces;
using TestingControllersSample.Core.Model;

namespace TestingControllersSample.Api
{
[Route("api/ideas")]
public class IdeasController : Controller
{
private readonly IBrainstormSessionRepository _sessionRepository;

public IdeasController(IBrainstormSessionRepository sessionRepository)


{
_sessionRepository = sessionRepository;
}

[HttpGet("forsession/{sessionId}")]
public async Task<IActionResult> ForSession(int sessionId)
{
var session = await _sessionRepository.GetByIdAsync(sessionId);
if (session == null)
{
return NotFound(sessionId);
}

var result = session.Ideas.Select(idea => new IdeaDTO()


{
Id = idea.Id,
Name = idea.Name,
Description = idea.Description,
DateCreated = idea.DateCreated
}).ToList();

return Ok(result);
}

[HttpPost("create")]
public async Task<IActionResult> Create([FromBody]NewIdeaModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}

var session = await _sessionRepository.GetByIdAsync(model.SessionId);


if (session == null)
{
return NotFound(model.SessionId);
}
var idea = new Idea()
{
DateCreated = DateTimeOffset.Now,
Description = model.Description,
Name = model.Name
};
session.AddIdea(idea);

await _sessionRepository.UpdateAsync(session);

return Ok(session);
}
}
}

El método ForSession devuelve una lista de tipos de IdeaDTO . Evite devolver entidades de dominio de empresa
directamente a través de llamadas API, ya que con frecuencia incluyen más datos de los que el cliente de API
requiere y asocian innecesariamente el modelo de dominio interno de su aplicación con la API que se expone
externamente. La asignación entre las entidades de dominio y los tipos que se van a devolver se puede realizar
manualmente (usando un método Select de LINQ, tal y como se muestra aquí) o por medio de una biblioteca
como AutoMapper.
Estas son las pruebas unitarias de los métodos API Create y ForSession :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Moq;
using TestingControllersSample.Api;
using TestingControllersSample.ClientModels;
using TestingControllersSample.Core.Interfaces;
using TestingControllersSample.Core.Model;
using Xunit;

namespace TestingControllersSample.Tests.UnitTests
{
public class ApiIdeasControllerTests
{
[Fact]
public async Task Create_ReturnsBadRequest_GivenInvalidModel()
{
// Arrange & Act
var mockRepo = new Mock<IBrainstormSessionRepository>();
var controller = new IdeasController(mockRepo.Object);
controller.ModelState.AddModelError("error","some error");

// Act
var result = await controller.Create(model: null);

// Assert
Assert.IsType<BadRequestObjectResult>(result);
}

[Fact]
public async Task Create_ReturnsHttpNotFound_ForInvalidSession()
{
// Arrange
int testSessionId = 123;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.Returns(Task.FromResult((BrainstormSession)null));
var controller = new IdeasController(mockRepo.Object);
// Act
var result = await controller.Create(new NewIdeaModel());

// Assert
Assert.IsType<NotFoundObjectResult>(result);
}

[Fact]
public async Task Create_ReturnsNewlyCreatedIdeaForSession()
{
// Arrange
int testSessionId = 123;
string testName = "test name";
string testDescription = "test description";
var testSession = GetTestSession();
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.Returns(Task.FromResult(testSession));
var controller = new IdeasController(mockRepo.Object);

var newIdea = new NewIdeaModel()


{
Description = testDescription,
Name = testName,
SessionId = testSessionId
};
mockRepo.Setup(repo => repo.UpdateAsync(testSession))
.Returns(Task.CompletedTask)
.Verifiable();

// Act
var result = await controller.Create(newIdea);

// Assert
var okResult = Assert.IsType<OkObjectResult>(result);
var returnSession = Assert.IsType<BrainstormSession>(okResult.Value);
mockRepo.Verify();
Assert.Equal(2, returnSession.Ideas.Count());
Assert.Equal(testName, returnSession.Ideas.LastOrDefault().Name);
Assert.Equal(testDescription, returnSession.Ideas.LastOrDefault().Description);
}

private BrainstormSession GetTestSession()


{
var session = new BrainstormSession()
{
DateCreated = new DateTime(2016, 7, 2),
Id = 1,
Name = "Test One"
};

var idea = new Idea() { Name = "One" };


session.AddIdea(idea);
return session;
}
}
}

Como se ha indicado anteriormente, si quiere comprobar el comportamiento del método cuando ModelState
no es válido, agregue un error de modelo al controlador como parte de la prueba. No intente probar la
validación del modelo o el enlace de modelos en las pruebas unitarias: céntrese tan solo en el comportamiento
de su método de acción al confrontarlo con un valor de ModelState determinado.
La segunda prueba depende de que el repositorio devuelva null, por lo que el repositorio ficticio está
configurado para devolver un valor null. No es necesario crear una base de datos de prueba (en memoria o de
cualquier otro modo) ni crear una consulta que devuelva este resultado. Esto se puede realizar en una sola
instrucción, tal y como se muestra.
La última prueba confirma que se llama al método Update del repositorio. Tal y como hicimos anteriormente,
se llama al objeto ficticio con Verifiable y, después, se llama al método Verify del repositorio ficticio para
confirmar que el método Verifiable se ha ejecutado. Las pruebas unitarias no se encargan de garantizar que el
método Update guarda los datos; esto se puede realizar con una prueba de integración.

Pruebas de integración
Las pruebas de integración se realizan para garantizar que distintos módulos independientes dentro de la
aplicación funcionan correctamente juntos. Por lo general, todo lo que se puede comprobar con una prueba
unitaria también se puede comprobar con una prueba de integración, pero no a la inversa. Pero las pruebas de
integración suelen ser mucho más lentas que las unitarias. Por tanto, lo mejor es comprobar todo lo que sea
factible con las pruebas unitarias y recurrir a las pruebas de integración en los casos en los que existan varios
colaboradores.
Los objetos ficticios rara vez se usan en las pruebas de integración, aunque pueden seguir siendo de utilidad. En
las pruebas unitarias, los objetos ficticios constituyen un método eficaz de controlar el modo en que los
colaboradores fuera de la unidad que se está probando deben comportarse según los propósitos de la prueba.
En una prueba de integración se usan colaboradores reales para confirmar que todo el subsistema funciona
correctamente en conjunto.
Estado de la aplicación
Una consideración importante al realizar pruebas de integración es cómo establecer el estado de la aplicación.
Las pruebas se deben ejecutar de manera independiente entre sí, por lo que cada prueba debe comenzar con la
aplicación en un estado conocido. Que la aplicación no use una base de datos o tenga algún tipo de persistencia
no debe ser un problema. Pero la mayoría de las aplicaciones reales almacenan su estado en algún tipo de
almacén de datos, de modo que cualquier modificación que se realice a raíz de una prueba puede repercutir en
otra, a menos que se restablezca el almacén de datos. Si se usa el método integrado TestServer , será muy fácil
hospedar aplicaciones ASP.NET Core en nuestras pruebas de integración, pero esto no da acceso
necesariamente a los datos que se van a usar. Si se usa una base de datos real, un método consiste en conectar
la aplicación a una base de datos de prueba, a la que las pruebas pueden obtener acceso, y confirmar que está
restablecida en un estado conocido antes de que cada prueba se ejecute.
En esta aplicación de ejemplo, uso la base de datos InMemoryDatabase de Entity Framework Core, por lo que
no puedo simplemente conectarme a ella desde mi proyecto de prueba. En su lugar, expondré un método
InitializeDatabase desde la clase Startup de la aplicación, y llamaré a ese método cuando la aplicación se
inicie si está en el entorno Development . Mis pruebas de integración sacarán partido de esto automáticamente
siempre que tengan el entorno establecido en Development . No tiene que preocuparse de restablecer la base de
datos, ya que InMemoryDatabase se restablece cada vez que la aplicación se reinicia.
La clase Startup :

using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using TestingControllersSample.Core.Interfaces;
using TestingControllersSample.Core.Model;
using TestingControllersSample.Infrastructure;

namespace TestingControllersSample
{
public class Startup
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<AppDbContext>(
optionsBuilder => optionsBuilder.UseInMemoryDatabase("InMemoryDb"));

services.AddMvc();

services.AddScoped<IBrainstormSessionRepository,
EFStormSessionRepository>();
}

public void Configure(IApplicationBuilder app,


IHostingEnvironment env,
ILoggerFactory loggerFactory)
{
if (env.IsDevelopment())
{
var repository = app.ApplicationServices.GetService<IBrainstormSessionRepository>();
InitializeDatabaseAsync(repository).Wait();
}

app.UseStaticFiles();

app.UseMvcWithDefaultRoute();
}

public async Task InitializeDatabaseAsync(IBrainstormSessionRepository repo)


{
var sessionList = await repo.ListAsync();
if (!sessionList.Any())
{
await repo.AddAsync(GetTestSession());
}
}

public static BrainstormSession GetTestSession()


{
var session = new BrainstormSession()
{
Name = "Test Session 1",
DateCreated = new DateTime(2016, 8, 1)
};
var idea = new Idea()
{
DateCreated = new DateTime(2016, 8, 1),
Description = "Totally awesome idea",
Name = "Awesome idea"
};
session.AddIdea(idea);
return session;
}
}
}

Verá que el método GetTestSession se usa con bastante asiduidad en las siguientes pruebas de integración.
Acceso a las vistas
En cada clase de prueba de integración se configura el método TestServer que ejecutará la aplicación de
ASP.NET Core. TestServer hospeda la aplicación web de forma predeterminada en la carpeta donde se está
ejecutando (en este caso, la carpeta del proyecto de prueba). Por tanto, si intenta probar las acciones del
controlador que devuelven ViewResult , es posible que aparezca este error:
The view 'Index' wasn't found. The following locations were searched:
(list of locations)

Para corregir este problema, debe configurar la raíz del contenido del servidor de forma que pueda localizar las
vistas del proyecto que se está comprobando. Esto se consigue con una llamada a UseContentRoot en la clase
TestFixture , como se aprecia aquí:

using System;
using System.IO;
using System.Net.Http;
using System.Reflection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.ViewComponents;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;

namespace TestingControllersSample.Tests.IntegrationTests
{
/// <summary>
/// A test fixture which hosts the target project (project we wish to test) in an in-memory server.
/// </summary>
/// <typeparam name="TStartup">Target project's startup type</typeparam>
public class TestFixture<TStartup> : IDisposable
{
private readonly TestServer _server;

public TestFixture()
: this(Path.Combine("src"))
{
}

protected TestFixture(string relativeTargetProjectParentDir)


{
var startupAssembly = typeof(TStartup).GetTypeInfo().Assembly;
var contentRoot = GetProjectPath(relativeTargetProjectParentDir, startupAssembly);

var builder = new WebHostBuilder()


.UseContentRoot(contentRoot)
.ConfigureServices(InitializeServices)
.UseEnvironment("Development")
.UseStartup(typeof(TStartup));

_server = new TestServer(builder);

Client = _server.CreateClient();
Client.BaseAddress = new Uri("http://localhost");
}

public HttpClient Client { get; }

public void Dispose()


{
Client.Dispose();
_server.Dispose();
}

protected virtual void InitializeServices(IServiceCollection services)


{
var startupAssembly = typeof(TStartup).GetTypeInfo().Assembly;

// Inject a custom application part manager.


// Overrides AddMvcCore() because it uses TryAdd().
var manager = new ApplicationPartManager();
manager.ApplicationParts.Add(new AssemblyPart(startupAssembly));
manager.ApplicationParts.Add(new AssemblyPart(startupAssembly));
manager.FeatureProviders.Add(new ControllerFeatureProvider());
manager.FeatureProviders.Add(new ViewComponentFeatureProvider());

services.AddSingleton(manager);
}

/// <summary>
/// Gets the full path to the target project that we wish to test
/// </summary>
/// <param name="projectRelativePath">
/// The parent directory of the target project.
/// e.g. src, samples, test, or test/Websites
/// </param>
/// <param name="startupAssembly">The target project's assembly.</param>
/// <returns>The full path to the target project.</returns>
private static string GetProjectPath(string projectRelativePath, Assembly startupAssembly)
{
// Get name of the target project which we want to test
var projectName = startupAssembly.GetName().Name;

// Get currently executing test project path


var applicationBasePath = System.AppContext.BaseDirectory;

// Find the path to the target project


var directoryInfo = new DirectoryInfo(applicationBasePath);
do
{
directoryInfo = directoryInfo.Parent;

var projectDirectoryInfo = new DirectoryInfo(Path.Combine(directoryInfo.FullName,


projectRelativePath));
if (projectDirectoryInfo.Exists)
{
var projectFileInfo = new FileInfo(Path.Combine(projectDirectoryInfo.FullName,
projectName, $"{projectName}.csproj"));
if (projectFileInfo.Exists)
{
return Path.Combine(projectDirectoryInfo.FullName, projectName);
}
}
}
while (directoryInfo.Parent != null);

throw new Exception($"Project root could not be located using the application root
{applicationBasePath}.");
}
}
}

La clase TestFixture se encarga de configurar y crear el método TestServer , que configura un HttpClient
para comunicarse con dicho método TestServer . En cada una de las pruebas de integración se usa la propiedad
Client para conectarse al servidor de prueba y realizar una solicitud.
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Xunit;

namespace TestingControllersSample.Tests.IntegrationTests
{
public class HomeControllerTests : IClassFixture<TestFixture<TestingControllersSample.Startup>>
{
private readonly HttpClient _client;

public HomeControllerTests(TestFixture<TestingControllersSample.Startup> fixture)


{
_client = fixture.Client;
}

[Fact]
public async Task ReturnsInitialListOfBrainstormSessions()
{
// Arrange - get a session known to exist
var testSession = Startup.GetTestSession();

// Act
var response = await _client.GetAsync("/");

// Assert
response.EnsureSuccessStatusCode();
var responseString = await response.Content.ReadAsStringAsync();
Assert.Contains(testSession.Name, responseString);
}

[Fact]
public async Task PostAddsNewBrainstormSession()
{
// Arrange
string testSessionName = Guid.NewGuid().ToString();
var data = new Dictionary<string, string>();
data.Add("SessionName", testSessionName);
var content = new FormUrlEncodedContent(data);

// Act
var response = await _client.PostAsync("/", content);

// Assert
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.Equal("/", response.Headers.Location.ToString());
}
}
}

En la primera prueba de arriba, responseString contiene el HTML realmente presentado de la vista, que se
puede revisar para confirmar que contiene los resultados esperados.
La segunda prueba crea un formulario POST con un nombre de sesión único y lo envía a la aplicación para,
seguidamente, confirmar que se devuelve la redirección prevista.
Métodos de API
Si la aplicación expone API web, conviene confirmar que se ejecutan según lo previsto por medio de pruebas
automatizadas. El método integrado TestServer permite comprobar API web de forma muy sencilla. Si los
métodos de API usan el enlace de modelos, deberá comprobar siempre el factor ModelState.IsValid y, en este
sentido, las pruebas de integración son el lugar adecuado para confirmar que la validación del modelo funciona
correctamente.
El siguiente conjunto de pruebas tiene como destino el método Create en la clase IdeasController de arriba:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Newtonsoft.Json;
using TestingControllersSample.ClientModels;
using TestingControllersSample.Core.Model;
using Xunit;

namespace TestingControllersSample.Tests.IntegrationTests
{
public class ApiIdeasControllerTests : IClassFixture<TestFixture<TestingControllersSample.Startup>>
{
internal class NewIdeaDto
{
public NewIdeaDto(string name, string description, int sessionId)
{
Name = name;
Description = description;
SessionId = sessionId;
}

public string Name { get; set; }


public string Description { get; set; }
public int SessionId { get; set; }
}

private readonly HttpClient _client;

public ApiIdeasControllerTests(TestFixture<TestingControllersSample.Startup> fixture)


{
_client = fixture.Client;
}

[Fact]
public async Task CreatePostReturnsBadRequestForMissingNameValue()
{
// Arrange
var newIdea = new NewIdeaDto("", "Description", 1);

// Act
var response = await _client.PostAsJsonAsync("/api/ideas/create", newIdea);

// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}

[Fact]
public async Task CreatePostReturnsBadRequestForMissingDescriptionValue()
{
// Arrange
var newIdea = new NewIdeaDto("Name", "", 1);

// Act
var response = await _client.PostAsJsonAsync("/api/ideas/create", newIdea);

// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}

[Fact]
public async Task CreatePostReturnsBadRequestForSessionIdValueTooSmall()
{
// Arrange
// Arrange
var newIdea = new NewIdeaDto("Name", "Description", 0);

// Act
var response = await _client.PostAsJsonAsync("/api/ideas/create", newIdea);

// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}

[Fact]
public async Task CreatePostReturnsBadRequestForSessionIdValueTooLarge()
{
// Arrange
var newIdea = new NewIdeaDto("Name", "Description", 1000001);

// Act
var response = await _client.PostAsJsonAsync("/api/ideas/create", newIdea);

// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}

[Fact]
public async Task CreatePostReturnsNotFoundForInvalidSession()
{
// Arrange
var newIdea = new NewIdeaDto("Name", "Description", 123);

// Act
var response = await _client.PostAsJsonAsync("/api/ideas/create", newIdea);

// Assert
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}

[Fact]
public async Task CreatePostReturnsCreatedIdeaWithCorrectInputs()
{
// Arrange
var testIdeaName = Guid.NewGuid().ToString();
var newIdea = new NewIdeaDto(testIdeaName, "Description", 1);

// Act
var response = await _client.PostAsJsonAsync("/api/ideas/create", newIdea);

// Assert
response.EnsureSuccessStatusCode();
var returnedSession = await response.Content.ReadAsJsonAsync<BrainstormSession>();
Assert.Equal(2, returnedSession.Ideas.Count);
Assert.Contains(testIdeaName, returnedSession.Ideas.Select(i => i.Name).ToList());
}

[Fact]
public async Task ForSessionReturnsNotFoundForBadSessionId()
{
// Arrange & Act
var response = await _client.GetAsync("/api/ideas/forsession/500");

// Assert
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}

[Fact]
public async Task ForSessionReturnsIdeasForValidSessionId()
{
// Arrange
var testSession = Startup.GetTestSession();

// Act
// Act
var response = await _client.GetAsync("/api/ideas/forsession/1");

// Assert
response.EnsureSuccessStatusCode();
var ideaList = JsonConvert.DeserializeObject<List<IdeaDTO>>(
await response.Content.ReadAsStringAsync());
var firstIdea = ideaList.First();
Assert.Equal(testSession.Ideas.First().Name, firstIdea.Name);
}
}
}

A diferencia de las pruebas de integración de acciones que devuelven vistas HTML, los métodos de API web
que devuelven resultados se suelen poder deserializar como objetos fuertemente tipados, tal y como arroja la
última prueba mostrada arriba. En este caso, la prueba deserializa el resultado en una instancia de
BrainstormSession y confirma que la idea se agregó correctamente a la colección de ideas.

En el proyecto de ejemplo de este artículo encontrará más ejemplos de pruebas de integración.


Solucionar problemas de proyectos de ASP.NET Core
22/06/2018 • 5 minutes to read • Edit Online

Por Rick Anderson


Los vínculos siguientes proporcionan instrucciones para solucionar problemas:
Solución de problemas de ASP.NET Core en Azure App Service
Solución de problemas de ASP.NET Core en IIS
Referencia de errores comunes de Azure App Service e IIS con ASP.NET Core
Conferencia NDC (Londres, 2018): Diagnosticar problemas en aplicaciones de ASP.NET Core
Blog de ASP.NET: Solucionar problemas de rendimiento de ASP.NET Core

Advertencias de .NET core SDK


Se instalan las versiones de 64 bits de .NET Core SDK y 32 bits
En el nuevo proyecto cuadro de diálogo para ASP.NET Core, verá la advertencia siguiente:

Se instalan las versiones de 32 y 64 bits de .NET Core SDK. Sólo las plantillas de las versiones de 64 bits
instaladas en ' C:\archivos de programa\dotnet\sdk\' se mostrará.

Esta advertencia aparece cuando (x86) 32 bits y las versiones de 64 bits (x 64) de la .NET Core SDK están
instalados. Ambas versiones pueden instalarse de motivos comunes son:
Descargar al instalador de .NET Core SDK con un equipo de 32 bits pero, a continuación, copiarla en y
originalmente había instalado en un equipo de 64 bits.
El SDK de núcleo de .NET de 32 bits se instaló por otra aplicación.
Se ha descargado e instalado una versión incorrecta.
Desinstale el SDK de núcleo de .NET de 32 bits para evitar esta advertencia. Desinstalar desde el Panel de
Control > programas y características > desinstalar o cambiar un programa. Si entiende por qué se produce
la advertencia y sus implicaciones, puede omitir la advertencia.
.NET Core SDK está instalado en varias ubicaciones
En el nuevo proyecto cuadro de diálogo para ASP.NET Core, verá la advertencia siguiente:

.NET Core SDK está instalado en varias ubicaciones. Sólo las plantillas de la SDK(s) instalado en ' C:\archivos
de programa\dotnet\sdk\' se mostrará.

Verá este mensaje cuando se tiene al menos una instalación de .NET Core SDK en un directorio fuera de
C:\archivos de programa\dotnet\sdk\. Normalmente esto sucede cuando se ha implementado el SDK de .NET
Core en una máquina con copiar y pegar en lugar del instalador MSI.
Desinstale el SDK de núcleo de .NET de 32 bits para evitar esta advertencia. Desinstalar desde el Panel de
Control > programas y características > desinstalar o cambiar un programa. Si entiende por qué se produce
la advertencia y sus implicaciones, puede omitir la advertencia.
No se detectaron ningún SDK de .NET Core
En el nuevo proyecto cuadro de diálogo para ASP.NET Core, verá la advertencia siguiente:

No se detectaron ningún SDK de .NET Core, asegúrese de que se incluyen en la variable de entorno 'PATH'.
Esta advertencia aparece cuando la variable de entorno PATH no apunta a los SDK de núcleo de .NET en el equipo.
Para resolver este problema:
Instalar o comprobar que está instalado el SDK de .NET Core.
Compruebe el PATH variable de entorno apunta a la ubicación que está instalado el SDK. El instalador
normalmente establece el PATH .
Uso de IHtmlHelper.Partial puede producir interbloqueos de aplicación
En ASP.NET Core 2.1 y versiones posteriores, al llamar a Html.Partial da como resultado una advertencia de
analizador debido a la posibilidad de interbloqueos. Es el mensaje de advertencia:

Uso de IHtmlHelper.Partial puede producir interbloqueos de la aplicación. Considere el uso de <partial>


auxiliar de etiqueta o IHtmlHelper.PartialAsync .

Las llamadas a @Html.Partial deben reemplazarse por @await Html.PartialAsync o la aplicación auxiliar de la
etiqueta parcial <partial name="_Partial" /> .
Trabajo con datos en ASP.NET Core
21/06/2018 • 2 minutes to read • Edit Online

Introducción a las páginas de Razor y Entity Framework Core con Visual Studio
Introducción a las páginas de Razor y EF
Operaciones de creación, lectura, actualización y eliminación
Ordenación, filtrado, paginación y agrupación
Migraciones
Creación de un modelo de datos complejo
Lectura de datos relacionados
Actualización de datos relacionados
Control de conflictos de simultaneidad
Introducción a ASP.NET Core MVC y Entity Framework Core con Visual Studio
Introducción
Operaciones de creación, lectura, actualización y eliminación
Ordenación, filtrado, paginación y agrupación
Migraciones
Creación de un modelo de datos complejo
Lectura de datos relacionados
Actualización de datos relacionados
Control de conflictos de simultaneidad
Herencia
Temas avanzados
ASP.NET Core con EF Core: nueva base de datos (sitio de la documentación de Entity Framework Core)
ASP.NET Core con EF Core: base de datos existente (sitio de la documentación de Entity Framework Core)
Introducción a ASP.NET Core y Entity Framework 6
Azure Storage
Agregar Azure Storage mediante el uso de Servicios conectados de Visual Studio
Introducción a Azure Blob Storage y Servicios conectados de Visual Studio
Introducción a Queue Storage y Servicios conectados de Visual Studio
Introducción a Azure Table Storage y Servicios conectados de Visual Studio
Páginas de Razor con Entity Framework Core en
ASP.NET Core: Tutorial 1 de 8
25/06/2018 • 32 minutes to read • Edit Online

Por Tom Dykstra y Rick Anderson


En la aplicación web de ejemplo Contoso University se muestra cómo crear aplicaciones web ASP.NET Core
2.0 MVC con Entity Framework (EF ) Core 2.0 y Visual Studio 2017.
La aplicación de ejemplo es un sitio web de una universidad ficticia, Contoso University. Incluye funciones
como la admisión de estudiantes, la creación de cursos y asignaciones de instructores. Esta página es la
primera de una serie de tutoriales en los que se explica cómo crear la aplicación de ejemplo Contoso
University.
Descargue o vea la aplicación completa. Instrucciones de descarga.

Requisitos previos
Install one of the following:
CLI tooling: Windows, Linux, or macOS: .NET Core SDK 2.0 or later
IDE/editor tooling
Windows: Visual Studio for Windows
ASP.NET and web development workload
.NET Core cross-platform development workload
Linux: Visual Studio Code
macOS: Visual Studio for Mac
Familiaridad con las Páginas de Razor. Los programadores nuevos deben completar Introducción a las
páginas de Razor en ASP.NET Core antes de empezar esta serie.

Solución de problemas
Si experimenta un problema que no puede resolver, por lo general podrá encontrar la solución si compara el
código con la fase completada. Para obtener una lista de errores comunes y cómo resolverlos, vea la sección
de solución de problemas del último tutorial de la serie. Si ahí no encuentra lo que necesita, puede publicar
una pregunta en StackOverflow.com para ASP.NET Core o EF Core.

TIP
Esta serie de tutoriales se basa en lo que se realiza en los tutoriales anteriores. Considere la posibilidad de guardar
una copia del proyecto después de completar correctamente cada tutorial. Si experimenta problemas, puede empezar
desde el tutorial anterior en lugar de volver al principio. Como alternativa, puede descargar una fase completada y
empezar de nuevo con la fase completada.

La aplicación web Contoso University


La aplicación compilada en estos tutoriales es un sitio web básico de una universidad.
Los usuarios pueden ver y actualizar la información de estudiantes, cursos e instructores. Estas son algunas
de las pantallas que se crean en el tutorial.
El estilo de la interfaz de usuario de este sitio se mantiene fiel a lo que generan las plantillas integradas. El
tutorial se centra en EF Core con páginas de Razor, no en la interfaz de usuario.

Creación de una aplicación web de páginas de Razor


En el menú Archivo de Visual Studio, seleccione Nuevo > Proyecto.
Cree una aplicación web de ASP.NET Core. Asigne el nombre ContosoUniversity al proyecto. Es
importante que el nombre del proyecto sea ContosoUniversity para que coincidan los espacios de
nombres al copiar y pegar el código.

Seleccione ASP.NET Core 2.0 en la lista desplegable y, luego, seleccione Aplicación web.

Presione F5 para ejecutar la aplicación en modo de depuración o Ctrl-F5 para que se ejecute sin adjuntar el
depurador.
Configurar el estilo del sitio
Con algunos cambios se configura el menú del sitio, el diseño y la página principal.
Abra Pages/_Layout.cshtml y realice los cambios siguientes:
Cambie todas las repeticiones de "ContosoUniversity" por "Contoso University". Hay tres
repeticiones.
Agregue entradas de menú para Students, Courses, Instructors y Departments, y elimine la
entrada de menú Contact.
Los cambios aparecen resaltados. (No se muestra todo el marcado).
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - Contoso University</title>

<environment include="Development">
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
<link rel="stylesheet" href="~/css/site.css" />
</environment>
<environment exclude="Development">
<link rel="stylesheet"
href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.min.css"
asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-
value="absolute" />
<link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" />
</environment>
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-
collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a asp-page="/Index" class="navbar-brand">Contoso University</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a asp-page="/Index">Home</a></li>
<li><a asp-page="/About">About</a></li>
<li><a asp-page="/Students/Index">Students</a></li>
<li><a asp-page="/Courses/Index">Courses</a></li>
<li><a asp-page="/Instructors/Index">Instructors</a></li>
<li><a asp-page="/Departments/Index">Departments</a></li>
</ul>
</div>
</div>
</nav>
<div class="container body-content">
@RenderBody()
<hr />
<footer>
<p>&copy; 2017 - Contoso University</p>
</footer>
</div>

En Pages/Index.cshtml, reemplace el contenido del archivo con el código siguiente para reemplazar el texto
sobre ASP.NET y MVC con texto sobre esta aplicación:
@page
@model IndexModel
@{
ViewData["Title"] = "Home page";
}

<div class="jumbotron">
<h1>Contoso University</h1>
</div>
<div class="row">
<div class="col-md-4">
<h2>Welcome to Contoso University</h2>
<p>
Contoso University is a sample application that
demonstrates how to use Entity Framework Core in an
ASP.NET Core Razor Pages web app.
</p>
</div>
<div class="col-md-4">
<h2>Build it from scratch</h2>
<p>You can build the application by following the steps in a series of tutorials.</p>
<p><a class="btn btn-default"
href="https://docs.microsoft.com/aspnet/core/data/ef-rp/intro">
See the tutorial &raquo;</a></p>
</div>
<div class="col-md-4">
<h2>Download it</h2>
<p>You can download the completed project from GitHub.</p>
<p><a class="btn btn-default"
href="https://github.com/aspnet/Docs/tree/master/aspnetcore/data/ef-rp/intro/samples/cu-
final">
See project source code &raquo;</a></p>
</div>
</div>

Presione CTRL+F5 para ejecutar el proyecto. La página principal se muestra con las pestañas creadas en los
tutoriales siguientes:
Crear el modelo de datos
Cree las clases de entidad para la aplicación Contoso University. Comience con las tres entidades siguientes:

Hay una relación uno a varios entre las entidades Student y Enrollment . Hay una relación uno a varios
entre las entidades Course y Enrollment . Un estudiante se puede inscribir en cualquier número de cursos.
Un curso puede tener cualquier número de alumnos inscritos.
En las secciones siguientes, se crea una clase para cada una de estas entidades.
La entidad Student

Cree una carpeta Models. En la carpeta Models, cree un archivo de clase denominado Student.cs con el
código siguiente:

using System;
using System.Collections.Generic;

namespace ContosoUniversity.Models
{
public class Student
{
public int ID { get; set; }
public string LastName { get; set; }
public string FirstMidName { get; set; }
public DateTime EnrollmentDate { get; set; }

public ICollection<Enrollment> Enrollments { get; set; }


}
}

La propiedad ID se convierte en la columna de clave principal de la tabla de base de datos (DB ) que
corresponde a esta clase. De forma predeterminada, EF Core interpreta como la clave principal una
propiedad que se denomine ID o classnameID . En classnameID , classname es el nombre de la clase (como
Student en el ejemplo anterior ).

La propiedad Enrollments es una propiedad de navegación. Las propiedades de navegación se vinculan a


otras entidades relacionadas con esta entidad. En este caso, la propiedad Enrollments de una
Student entity contiene todas las entidades Enrollment que están relacionadas con esa entidad Student .
Por ejemplo, si una fila Student de la base de datos tiene dos filas Enrollment relacionadas, la propiedad de
navegación Enrollments contiene esas dos entidades Enrollment . Una fila Enrollment relacionada es la
que contiene el valor de clave principal de ese estudiante en la columna StudentID . Por ejemplo, suponga
que el estudiante con ID=1 tiene dos filas en la tabla Enrollment . La tabla Enrollment tiene dos filas con
StudentID = 1. StudentID es una clave externa en la tabla Enrollment que especifica el estudiante en la
tabla Student .
Si una propiedad de navegación puede contener varias entidades, la propiedad de navegación debe ser un
tipo de lista, como ICollection<T> . Se puede especificar ICollection<T> , o bien un tipo como List<T> o
HashSet<T> . Cuando se usa ICollection<T> , EF Core crea una colección HashSet<T> de forma
predeterminada. Las propiedades de navegación que contienen varias entidades proceden de relaciones de
varios a varios y uno a varios.
La entidad Enrollment
En la carpeta Models, cree Enrollment.cs con el código siguiente:

namespace ContosoUniversity.Models
{
public enum Grade
{
A, B, C, D, F
}

public class Enrollment


{
public int EnrollmentID { get; set; }
public int CourseID { get; set; }
public int StudentID { get; set; }
public Grade? Grade { get; set; }

public Course Course { get; set; }


public Student Student { get; set; }
}
}

La propiedad EnrollmentID es la clave principal. En esta entidad se usa el patrón classnameID en lugar de
ID como en la entidad Student . Normalmente, los desarrolladores eligen un patrón y lo usan en todo el
modelo de datos. En un tutorial posterior, se muestra el uso de ID sin un nombre de clase para facilitar la
implementación de la herencia en el modelo de datos.
La propiedad Grade es una enum . El signo de interrogación después de la declaración de tipo Grade indica
que la propiedad Grade acepta valores NULL. Una calificación que sea NULL es diferente de una
calificación que sea cero; NULL significa que no se conoce una calificación o que todavía no se ha asignado.
La propiedad StudentID es una clave externa y la propiedad de navegación correspondiente es Student .
Una entidad Enrollment está asociada con una entidad Student , por lo que la propiedad contiene una
única entidad Student . La entidad Student difiere de la propiedad de navegación Student.Enrollments , que
contiene varias entidades Enrollment .
La propiedad CourseID es una clave externa y la propiedad de navegación correspondiente es Course . Una
entidad Enrollment está asociada con una entidad Course .
EF Core interpreta una propiedad como una clave externa si se denomina
<navigation property name><primary key property name> . Por ejemplo, StudentID para la propiedad de
navegación Student , puesto que la clave principal de la entidad Student es ID . Las propiedades de clave
externa también se pueden denominar <primary key property name> . Por ejemplo CourseID , dado que la
clave principal de la entidad Course es CourseID .
La entidad Course
En la carpeta Models, cree Course.cs con el código siguiente:

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
public class Course
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int CourseID { get; set; }
public string Title { get; set; }
public int Credits { get; set; }

public ICollection<Enrollment> Enrollments { get; set; }


}
}

La propiedad Enrollments es una propiedad de navegación. Una entidad Course puede estar relacionada
con cualquier número de entidades Enrollment .
El atributo DatabaseGenerated permite que la aplicación especifique la clave principal en lugar de hacer que
la base de datos la genere.

Crear el contexto de base de datos SchoolContext


La clase principal que coordina la funcionalidad de EF Core para un modelo de datos determinado es la
clase de contexto de base de datos. El contexto de datos se deriva de
Microsoft.EntityFrameworkCore.DbContext . En el contexto de datos se especifica qué entidades se incluyen en
el modelo de datos. En este proyecto, la clase se denomina SchoolContext .
En la carpeta del proyecto, cree una carpeta denominada Data.
En la carpeta Data, cree SchoolContext.cs con el código siguiente:

using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity.Data
{
public class SchoolContext : DbContext
{
public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
{
}

public DbSet<Course> Courses { get; set; }


public DbSet<Enrollment> Enrollments { get; set; }
public DbSet<Student> Students { get; set; }
}
}
Este código crea una propiedad DbSet para cada conjunto de entidades. En la terminología de EF Core:
Un conjunto de entidades normalmente se corresponde a una tabla de base de datos.
Una entidad se corresponde con una fila de la tabla.
DbSet<Enrollment> y DbSet<Course> se pueden omitir. EF Core las incluye implícitamente porque la entidad
Student hace referencia a la entidad Enrollment y la entidad Enrollment hace referencia a la entidad
Course . Para este tutorial, conserve DbSet<Enrollment> y DbSet<Course> en el SchoolContext .

Cuando se crea la base de datos, EF Core crea las tablas con los mismos nombres que los nombres de
propiedad DbSet . Los nombres de propiedad para las colecciones normalmente están en plural (Students
en lugar de Student). Los desarrolladores están en desacuerdo sobre si los nombres de tabla deben estar en
plural. Para estos tutoriales, se invalida el comportamiento predeterminado mediante la especificación de
nombres de tabla en singular en DbContext. Para especificar los nombres de tabla en singular, agregue el
código resaltado siguiente:

using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity.Data
{
public class SchoolContext : DbContext
{
public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
{
}

public DbSet<Course> Courses { get; set; }


public DbSet<Enrollment> Enrollments { get; set; }
public DbSet<Student> Students { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)


{
modelBuilder.Entity<Course>().ToTable("Course");
modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
modelBuilder.Entity<Student>().ToTable("Student");
}
}
}

Registro del contexto con inserción de dependencias


ASP.NET Core incluye la inserción de dependencias. Los servicios (como el contexto de base de datos de EF
Core) se registran con inserción de dependencias durante el inicio de la aplicación. Estos servicios se
proporcionan a los componentes que los necesitan (como las páginas de Razor) a través de parámetros de
constructor. El código de constructor que obtiene una instancia de contexto de base de datos se muestra
más adelante en el tutorial.
Para registrar SchoolContext como servicio, abra Startup.cs y agregue las líneas resaltadas al método
ConfigureServices .

public void ConfigureServices(IServiceCollection services)


{
services.AddDbContext<SchoolContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

services.AddMvc();
}
El nombre de la cadena de conexión se pasa al contexto mediante una llamada a un método en un objeto
DbContextOptionsBuilder . Para el desarrollo local, el sistema de configuración de ASP.NET Core lee la
cadena de conexión desde el archivo appsettings.json.
Agregue instrucciones usingpara los espacios de nombres ContosoUniversity.Data y
Microsoft.EntityFrameworkCore . Compile el proyecto.

using ContosoUniversity.Data;
using Microsoft.EntityFrameworkCore;

Abra el archivo appsettings.json y agregue una cadena de conexión como se muestra en el código siguiente:

{
"ConnectionStrings": {
"DefaultConnection": "Server=
(localdb)\\mssqllocaldb;Database=ContosoUniversity1;ConnectRetryCount=0;Trusted_Connection=True;Multiple
ActiveResultSets=true"
},
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Warning"
}
}
}

En la cadena de conexión anterior se usa ConnectRetryCount=0 para evitar que SQLClient se bloquee.
SQL Server Express LocalDB
La cadena de conexión especifica una base de datos de SQL Server LocalDB. LocalDB es una versión ligera
del motor de base de datos de SQL Server Express y está dirigida al desarrollo de aplicaciones, no al uso en
producción. LocalDB se inicia a petición y se ejecuta en modo de usuario, sin necesidad de una
configuración compleja. De forma predeterminada, LocalDB crea archivos de base de datos .mdf en el
directorio C:/Users/<user> .

Agregar código para inicializar la base de datos con datos de


prueba
EF Core crea una base de datos vacía. En esta sección, se escribe un método Seed para rellenarla con datos
de prueba.
En la carpeta Data, cree un archivo de clase denominado DbInitializer.cs y agregue el código siguiente:

using ContosoUniversity.Models;
using System;
using System.Linq;

namespace ContosoUniversity.Data
{
public static class DbInitializer
{
public static void Initialize(SchoolContext context)
{
context.Database.EnsureCreated();

// Look for any students.


if (context.Students.Any())
{
return; // DB has been seeded
}

var students = new Student[]


{
new Student{FirstMidName="Carson",LastName="Alexander",EnrollmentDate=DateTime.Parse("2005-
09-01")},
new Student{FirstMidName="Meredith",LastName="Alonso",EnrollmentDate=DateTime.Parse("2002-
09-01")},
new Student{FirstMidName="Arturo",LastName="Anand",EnrollmentDate=DateTime.Parse("2003-09-
01")},
new Student{FirstMidName="Gytis",LastName="Barzdukas",EnrollmentDate=DateTime.Parse("2002-
09-01")},
new Student{FirstMidName="Yan",LastName="Li",EnrollmentDate=DateTime.Parse("2002-09-01")},
new Student{FirstMidName="Peggy",LastName="Justice",EnrollmentDate=DateTime.Parse("2001-09-
01")},
new Student{FirstMidName="Laura",LastName="Norman",EnrollmentDate=DateTime.Parse("2003-09-
01")},
new Student{FirstMidName="Nino",LastName="Olivetto",EnrollmentDate=DateTime.Parse("2005-09-
01")}
};
foreach (Student s in students)
{
context.Students.Add(s);
}
context.SaveChanges();

var courses = new Course[]


{
new Course{CourseID=1050,Title="Chemistry",Credits=3},
new Course{CourseID=4022,Title="Microeconomics",Credits=3},
new Course{CourseID=4041,Title="Macroeconomics",Credits=3},
new Course{CourseID=1045,Title="Calculus",Credits=4},
new Course{CourseID=3141,Title="Trigonometry",Credits=4},
new Course{CourseID=2021,Title="Composition",Credits=3},
new Course{CourseID=2042,Title="Literature",Credits=4}
};
foreach (Course c in courses)
{
context.Courses.Add(c);
}
context.SaveChanges();

var enrollments = new Enrollment[]


{
new Enrollment{StudentID=1,CourseID=1050,Grade=Grade.A},
new Enrollment{StudentID=1,CourseID=4022,Grade=Grade.C},
new Enrollment{StudentID=1,CourseID=4041,Grade=Grade.B},
new Enrollment{StudentID=2,CourseID=1045,Grade=Grade.B},
new Enrollment{StudentID=2,CourseID=3141,Grade=Grade.F},
new Enrollment{StudentID=2,CourseID=2021,Grade=Grade.F},
new Enrollment{StudentID=3,CourseID=1050},
new Enrollment{StudentID=4,CourseID=1050},
new Enrollment{StudentID=4,CourseID=4022,Grade=Grade.F},
new Enrollment{StudentID=5,CourseID=4041,Grade=Grade.C},
new Enrollment{StudentID=6,CourseID=1045},
new Enrollment{StudentID=7,CourseID=3141,Grade=Grade.A},
};
foreach (Enrollment e in enrollments)
{
context.Enrollments.Add(e);
}
context.SaveChanges();
}
}
}

El código comprueba si hay estudiantes en la base de datos. Si no hay ningún estudiante en la base de
datos, se inicializa con datos de prueba. Carga los datos de prueba en matrices en lugar de colecciones
List<T> para optimizar el rendimiento.

El método EnsureCreated crea automáticamente la base de datos para el contexto de base de datos. Si la
base de datos existe, EnsureCreated vuelve sin modificarla.
En Program.cs, modifique el método Main para que haga lo siguiente:
Obtener una instancia del contexto de base de datos desde el contenedor de inserción de dependencias.
Llamar al método de inicialización, pasándolo al contexto.
Eliminar el contexto cuando el método de inicialización finalice.
En el código siguiente se muestra el archivo Program.cs actualizado.

// Unused usings removed


using System;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.DependencyInjection;
using ContosoUniversity.Data;

namespace ContosoUniversity
{
public class Program
{
public static void Main(string[] args)
{
var host = BuildWebHost(args);

using (var scope = host.Services.CreateScope())


{
var services = scope.ServiceProvider;
try
{
var context = services.GetRequiredService<SchoolContext>();
DbInitializer.Initialize(context);
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred while seeding the database.");
}
}

host.Run();
}

public static IWebHost BuildWebHost(string[] args) =>


WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.Build();
}
}

La primera vez que se ejecuta la aplicación, se crea la base de datos y se inicializa con datos de prueba.
Cuando se actualice el modelo de datos:
Se elimina la base de datos.
Se actualiza el método de inicialización.
Se ejecuta la aplicación y se crea una base de datos inicializada.
En los tutoriales posteriores, la base de datos se actualiza cuando cambia el modelo de datos, sin tener que
eliminarla y volver a crearla.

Agregar herramientas de scaffolding


En esta sección, se usa la Consola del Administrador de paquetes (PMC ) para agregar el paquete de
generación de código web de Visual Studio. Este paquete es necesario para ejecutar el motor de scaffolding.
En el menú Herramientas, seleccione Administrador de paquetes NuGet > Consola del
Administrador de paquetes.
En la Consola del Administrador de paquetes (PMC ), escriba los comandos siguientes:

Install-Package Microsoft.VisualStudio.Web.CodeGeneration.Design
Install-Package Microsoft.VisualStudio.Web.CodeGeneration.Utils

El comando anterior agrega los paquetes NuGet al archivo *.csproj:

<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Utils" Version="2.0.0" />
</ItemGroup>
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.0" />
</ItemGroup>
</Project>

Aplicar scaffolding al modelo


Abra una ventana de comandos en el directorio del proyecto (el directorio que contiene los archivos
Program.cs, Startup.cs y .csproj).
Ejecute los comandos siguientes:

dotnet restore
dotnet tool install --global dotnet-aspnet-codegenerator --version 2.1.0
dotnet aspnet-codegenerator razorpage -m Student -dc SchoolContext -udl -outDir Pages\Students --
referenceScriptLibraries

Si se produce un error:

No executable found matching command "dotnet-aspnet-codegenerator"

Abra una ventana de comandos en el directorio del proyecto (el directorio que contiene los archivos
Program.cs, Startup.cs y .csproj).
Compile el proyecto. La compilación genera errores similares a los siguientes:
1>Pages\Students\Index.cshtml.cs(26,38,26,45): error CS1061: 'SchoolContext' does not contain a
definition for 'Student'

Cambie globalmente _context.Student por _context.Students (es decir, agregue una "s" a Student ). Se
encuentran y actualizan siete repeticiones. Esperamos solucionar este problema en la próxima versión.
En la tabla siguiente se incluyen los detalles de los parámetros de los generadores de código de ASP.NET
Core:

PARÁMETRO DESCRIPTION

-m Nombre del modelo

-dc Contexto de datos

-udl Usa el diseño predeterminado.

-outDir Ruta de acceso relativa de la carpeta de salida para crear


las vistas

--referenceScriptLibraries Agrega _ValidationScriptsPartial a las páginas Editar


y Crear.

Active o desactive h para obtener ayuda con el comando aspnet-codegenerator razorpage :

dotnet aspnet-codegenerator razorpage -h

Prueba de la aplicación
Ejecute la aplicación y haga clic en el vínculo Students. Según el ancho del explorador, el vínculo Students
aparece en la parte superior de la página. Si el vínculo Students no se ve, haga clic en el icono de
navegación en la esquina superior derecha.

Pruebe los vínculos Create, Edit y Details.

Ver la base de datos


Cuando se inicia la aplicación, DbInitializer.Initialize llama a EnsureCreated . EnsureCreated detecta si la
base de datos existe y crea una si es necesario. Si no hay ningún estudiante en la base de datos, el método
Initialize los agrega.

Abra el Explorador de objetos de SQL Server (SSOX) desde el menú Vista en Visual Studio. En SSOX,
haga clic en (localdb)\MSSQLLocalDB > Databases > ContosoUniversity1.
Expanda el nodo Tablas.
Haga clic con el botón derecho en la tabla Student y haga clic en Ver datos para ver las columnas que se
crearon y las filas que se insertaron en la tabla.
Los archivos de base de datos .mdf y .ldf se encuentran en la carpeta C:\Usuarios\.
EnsureCreated se llama durante el inicio de la aplicación, lo que permite el flujo de trabajo siguiente:
Se elimina la base de datos.
Se cambia el esquema de base de datos (por ejemplo, se agrega un campo EmailAddress ).
Ejecute la aplicación.
EnsureCreated crea una base de datos con la columna EmailAddress .

Convenciones
La cantidad de código que se escribe para que EF Core cree una base de datos completa es mínima debido
al uso de convenciones o a las suposiciones que hace EF Core.
Los nombres de las propiedades DbSet se usan como nombres de tabla. Para las entidades a las que
no se hace referencia con una propiedad DbSet , los nombres de clase de entidad se usan como
nombres de tabla.
Los nombres de propiedad de entidad se usan para los nombres de columna.
Las propiedades de entidad que se denominan ID o classnameID se reconocen como propiedades de
clave principal.
Una propiedad se interpreta como propiedad de clave externa si se denomina (por ejemplo,
StudentID para la propiedad de navegación Student , dado que la clave principal de la entidad
Student es ID ). Las propiedades de clave externa también se pueden denominar (por ejemplo
EnrollmentID , dado que la clave principal de la entidad Enrollment es EnrollmentID ).

El comportamiento de las convenciones se puede reemplazar. Por ejemplo, los nombres de tabla se pueden
especificar explícitamente, como se muestra anteriormente en este tutorial. Los nombres de columna se
pueden establecer explícitamente. Las claves principales y las claves externas se pueden establecer
explícitamente.

Código asincrónico
La programación asincrónica es el modo predeterminado de ASP.NET Core y EF Core.
Un servidor web tiene un número limitado de subprocesos disponibles y, en situaciones de carga alta, es
posible que todos los subprocesos disponibles estén en uso. Cuando esto ocurre, el servidor no puede
procesar nuevas solicitudes hasta que los subprocesos se liberen. Con el código sincrónico, se pueden
acumular muchos subprocesos mientras no estén realizando ningún trabajo porque están a la espera de que
finalice la E/S. Con el código asincrónico, cuando un proceso está a la espera de que finalice la E/S, se libera
su subproceso para el que el servidor lo use para el procesamiento de otras solicitudes. Como resultado, el
código asincrónico permite que los recursos de servidor se usen de forma más eficaz, y el servidor está
habilitado para administrar más tráfico sin retrasos.
El código asincrónico introduce una pequeña cantidad de sobrecarga en tiempo de ejecución. En situaciones
de poco tráfico, la disminución del rendimiento es insignificante, mientras que en situaciones de tráfico
elevado, la posible mejora del rendimiento es importante.
En el código siguiente, la palabra clave async , el valor devuelto Task<T> , la palabra clave await y el
método ToListAsync hacen que el código se ejecute de forma asincrónica.
public async Task OnGetAsync()
{
Student = await _context.Students.ToListAsync();
}

La palabra clave async indica al compilador que:


Genere devoluciones de llamada para partes del cuerpo del método.
Cree automáticamente el objeto Task que se devuelve. Para más información, vea Tipo de valor
devuelto Task.
El tipo devuelto implícito Task representa el trabajo en curso.
La palabra clave await hace que el compilador divida el método en dos partes. La primera parte
termina con la operación que se inició de forma asincrónica. La segunda parte se coloca en un
método de devolución de llamada que se llama cuando finaliza la operación.
ToListAsync es la versión asincrónica del método de extensión ToList .

Algunos aspectos que tener en cuenta al escribir código asincrónico en el que se usa EF Core son los
siguientes:
Solo se ejecutan de forma asincrónica las instrucciones que hacen que las consultas o los comandos
se envíen a la base de datos. Esto incluye ToListAsync , SingleOrDefaultAsync , FirstOrDefaultAsync y
SaveChangesAsync . No incluye las instrucciones que solo cambian una IQueryable , como
var students = context.Students.Where(s => s.LastName == "Davolio") .

Un contexto de EF Core no es seguro para subprocesos: no intente realizar varias operaciones en


paralelo.
Para aprovechar las ventajas de rendimiento del código asincrónico, compruebe que en los paquetes
de biblioteca (por ejemplo para paginación) se usa async si llaman a métodos de EF Core que envían
consultas a la base de datos.
Para obtener más información sobre la programación asincrónica en .NET, vea Información general de
Async.
En el siguiente tutorial, se examinan las operaciones CRUD (crear, leer, actualizar y eliminar) básicas.

S IG U IE N T E
ASP.NET Core MVC con EF Core: serie de tutoriales
21/06/2018 • 2 minutes to read • Edit Online

En este tutorial se explica el funcionamiento de ASP.NET Core MVC y Entity Framework Core con controladores y
vistas. Las páginas de Razor son una nueva alternativa en ASP.NET Core 2.0, un modelo de programación basado
en páginas que facilita la compilación de interfaces de usuario web y hace que sean más productivas.
Recomendamos el tutorial sobre las páginas de Razor antes que la versión MVC. El tutorial de las páginas de
Razor:
Es más fácil de seguir.
Proporciona más procedimientos recomendados de EF Core.
Usa consultas más eficaces.
Es más actual en relación con la API más reciente.
Abarca más características.
Es el método preferido para el desarrollo de nuevas aplicaciones.
Si elige este tutorial en lugar de la versión de páginas de Razor, le agradeceremos que nos explique el motivo en
este problema de GitHub.
1. Introducción
2. Operaciones de creación, lectura, actualización y eliminación
3. Ordenado, filtrado, paginación y agrupación
4. Migraciones
5. Creación de un modelo de datos complejo
6. Lectura de datos relacionados
7. Actualización de datos relacionados
8. Control de conflictos de simultaneidad
9. Herencia
10. Temas avanzados
Introducción a ASP.NET Core y Entity Framework 6
25/06/2018 • 7 minutes to read • Edit Online

Por Paweł Grudzień, Damien Pontifex y Tom Dykstra


En este artículo se muestra cómo usar Entity Framework 6 en una aplicación ASP.NET Core.

Información general
Para usar Entity Framework 6, el proyecto se tiene que compilar con .NET Framework, dado que Entity
Framework 6 no es compatible con .NET Core. Si necesita usar características multiplataforma, debe actualizar a
Entity Framework Core.
La manera recomendada de usar Entity Framework 6 en una aplicación ASP.NET Core es incluir el contexto y las
clases de modelo de EF6 en un proyecto de biblioteca de clases que tenga como destino la plataforma completa.
Agregue una referencia a la biblioteca de clases desde el proyecto de ASP.NET Core. Vea el ejemplo Visual Studio
solution with EF6 and ASP.NET Core projects (Solución de Visual Studio con proyectos de EF6 y ASP.NET Core).
No se puede colocar un contexto de EF6 en un proyecto de ASP.NET Core porque los proyectos de .NET Core no
admiten toda la funcionalidad que requieren los comandos de EF6 como Enable-Migrations.
Independientemente del tipo de proyecto en el que localice el contexto de EF6, solo las herramientas de línea de
comandos de EF6 funcionan con un contexto de EF6. Por ejemplo, Scaffold-DbContext solo está disponible en
Entity Framework Core. Si necesita la utilización de técnicas de ingeniería inversa para una base de datos en un
modelo de EF6, vea Code First to an Existing Database (Code First para una base de datos existente).

Marco de referencia completo y EF6 en el proyecto de ASP.NET Core


El proyecto de ASP.NET Core debe hacer referencia a .NET Framework y EF6. Por ejemplo, el archivo .csproj del
proyecto ASP.NET Core tendrá un aspecto similar al ejemplo siguiente (solo se muestran las partes relevantes del
archivo).

<PropertyGroup>
<TargetFramework>net452</TargetFramework>
<PreserveCompilationContext>true</PreserveCompilationContext>
<AssemblyName>MVCCore</AssemblyName>
<OutputType>Exe</OutputType>
<PackageId>MVCCore</PackageId>
</PropertyGroup>

Al crear un proyecto, use la plantilla Aplicación web ASP.NET Core (.NET Framework).

Controlar las cadenas de conexión


Las herramientas de línea de comandos de EF6 que se van a usar en el proyecto de biblioteca de clases de EF6
requieren un constructor predeterminado para poder crear instancias del contexto. Pero, probablemente querrá
especificar la cadena de conexión que se va a usar en el proyecto de ASP.NET Core, en cuyo caso el constructor de
contexto debe tener un parámetro que permita pasar la cadena de conexión. Este es un ejemplo.
public class SchoolContext : DbContext
{
public SchoolContext(string connString) : base(connString)
{
}

Como el contexto de EF6 no tiene un constructor sin parámetros, el proyecto de EF6 tiene que proporcionar una
implementación de IDbContextFactory. Las herramientas de línea de comandos de EF6 buscarán y usarán esa
implementación para poder crear instancias del contexto. Este es un ejemplo.

public class SchoolContextFactory : IDbContextFactory<SchoolContext>


{
public SchoolContext Create()
{
return new EF6.SchoolContext("Server=
(localdb)\\mssqllocaldb;Database=EF6MVCCore;Trusted_Connection=True;MultipleActiveResultSets=true");
}
}

En este ejemplo de código, la implementación de IDbContextFactory pasa una cadena de conexión codificada de
forma rígida. Se trata de la cadena de conexión que van a usar las herramientas de línea de comandos. Querrá
implementar una estrategia para asegurarse de que la biblioteca de clases usa la misma cadena de conexión que la
aplicación que realiza la llamada. Por ejemplo, podría obtener el valor de una variable de entorno en los dos
proyectos.

Configurar la inserción de dependencias en el proyecto de ASP.NET


Core
En el archivo Startup.cs del proyecto de Core, establezca el contexto de EF6 para la inserción de dependencias (DI)
en ConfigureServices . La duración de cada solicitud se debe configurar como ámbito de los objetos de contexto
de EF.

public void ConfigureServices(IServiceCollection services)


{
// Add framework services.
services.AddMvc();
services.AddScoped<SchoolContext>(_ => new
SchoolContext(Configuration.GetConnectionString("DefaultConnection")));
}

Después, puede obtener una instancia del contexto en los controladores mediante DI. El código es similar al que
escribiría para un contexto de EF Core:

public class StudentsController : Controller


{
private readonly SchoolContext _context;

public StudentsController(SchoolContext context)


{
_context = context;
}

Aplicación de ejemplo
Para obtener una aplicación de ejemplo funcional, vea la solución de Visual Studio de ejemplo que se incluye en
este artículo.
Este ejemplo se puede crear desde cero mediante los pasos siguientes en Visual Studio:
Cree una solución.
Agregar nuevo proyecto > Web > Aplicación web ASP.NET Core (.NET Framework)
Agregar nuevo proyecto > Escritorio clásico de Windows > Biblioteca de clases (.NET Framework)
En la Consola del Administrador de paquetes (PMC ) para ambos proyectos, ejecute el comando
Install-Package Entityframework .

En el proyecto de biblioteca de clases, cree clases de modelo de datos, una clase de contexto y una
implementación de IDbContextFactory .
En PMC para el proyecto de biblioteca de clases, ejecute los comandos Enable-Migrations y
Add-Migration Initial . Si ha configurado el proyecto de ASP.NET Core como proyecto de inicio, agregue
-StartupProjectName EF6 a estos comandos.

En el proyecto de Core, agregue una referencia de proyecto al proyecto de biblioteca de clases.


En el proyecto de Core, en Startup.cs, registre el contexto para DI.
En el proyecto de Core, en appsettings.json, agregue la cadena de conexión.
En el proyecto de Core, agregue un controlador y vistas para comprobar que puede leer y escribir datos.
(Tenga en cuenta que el scaffolding de ASP.NET Core MVC no funcionará con el contexto de EF6 al que se
hace referencia desde la biblioteca de clases).

Resumen
En este artículo se proporcionan instrucciones básicas para el uso de Entity Framework 6 en una aplicación
ASP.NET Core.

Recursos adicionales
Entity Framework - Code-Based Configuration (Entity Framework: configuración basada en código)
Azure Storage en ASP.NET Core
21/06/2018 • 2 minutes to read • Edit Online

Adición de Azure Storage mediante el uso de Servicios conectados de Visual Studio


Introducción a Blob Storage y Servicios conectados de Visual Studio
Introducción a Queue Storage y Servicios conectados de Visual Studio
Introducción a Table Storage y Servicios conectados de Visual Studio
Desarrollo del lado cliente en ASP.NET Core
21/06/2018 • 2 minutes to read • Edit Online

Uso de Gulp
Uso de Grunt
Administración de paquetes de cliente con Bower
Creación de sitios con capacidad de respuesta con Bootstrap
Aplicación de estilo a aplicaciones con LESS, Sass y Font Awesome
Agrupar y minimizar
TypeScript
Uso de Vínculo con exploradores
Uso de JavaScriptServices para aplicaciones de página única
Uso de plantillas de proyectos de aplicaciones de página única
Plantilla de proyecto Angular
Plantilla de proyecto React
Plantilla de proyecto React con Redux
Usar Gulp en ASP.NET Core
22/06/2018 • 18 minutes to read • Edit Online

Por Erik Reitan, Scott Addie, Daniel Roth, y Shayne Boyer


En una aplicación web moderna típica, el proceso de compilación puede:
Agrupar y minificar archivos JavaScript y CSS.
Ejecutar herramientas para llamar a las tareas de agrupación y minificación antes de cada compilación.
Compilar menos o SASS archivos CSS.
Compila los archivos de CoffeeScript o TypeScript a JavaScript.
A ejecutor de tareas es una herramienta que automatiza estas tareas de desarrollo de rutinas y mucho más. Visual
Studio proporciona compatibilidad integrada para dos corredores populares tareas basadas en JavaScript: Gulp y
Grunt.

gulp
Gulp es un toolkit de compilación streaming basadas en JavaScript para el código de cliente. Normalmente se
utiliza para secuenciar los archivos de cliente a través de una serie de procesos cuando se desencadena un evento
específico en un entorno de compilación. Por ejemplo, Gulp puede utilizarse para automatizar agrupar y minificar
o la limpieza de un entorno de desarrollo antes de una nueva compilación.
Se define un conjunto de tareas de Gulp en gulpfile.js. El siguiente código de JavaScript incluye módulos de Gulp
y especifica las rutas de acceso de archivo que se haga referencia dentro de las tareas disponibles próximamente:

/// <binding Clean='clean' />


"use strict";

var gulp = require("gulp"),


rimraf = require("rimraf"),
concat = require("gulp-concat"),
cssmin = require("gulp-cssmin"),
uglify = require("gulp-uglify");

var paths = {
webroot: "./wwwroot/"
};

paths.js = paths.webroot + "js/**/*.js";


paths.minJs = paths.webroot + "js/**/*.min.js";
paths.css = paths.webroot + "css/**/*.css";
paths.minCss = paths.webroot + "css/**/*.min.css";
paths.concatJsDest = paths.webroot + "js/site.min.js";
paths.concatCssDest = paths.webroot + "css/site.min.css";

El código anterior especifica qué módulos de nodo se necesitan. El require función importa cada módulo para
que las tareas dependientes pueden utilizar sus características. Cada uno de los módulos importados se asigna a
una variable. Los módulos pueden encontrarse por nombre o ruta de acceso. En este ejemplo, los módulos
denominan gulp , rimraf , gulp-concat , gulp-cssmin , y gulp-uglify se recuperan por su nombre. Además, se
crean una serie de rutas de acceso de modo que las ubicaciones de los archivos CSS y JavaScript se pueden
volver a usar y hace referencia en las tareas. En la tabla siguiente se proporciona descripciones de los módulos
incluidos en gulpfile.js.
NOMBRE DEL MÓDULO DESCRIPCIÓN

gulp El sistema de compilación streaming Gulp. Para obtener más


información, consulte gulp.

rimraf Un módulo de eliminación de nodo. Para obtener más


información, consulte rimraf.

gulp concat Un módulo que concatena los archivos según su carácter de


nueva línea del sistema operativo. Para obtener más
información, consulte gulp concat.

gulp cssmin Un módulo que minifica objeto archivos CSS. Para obtener
más información, consulte gulp cssmin.

uglify gulp Un módulo que minifica objeto .js archivos. Para obtener más
información, consulte uglify gulp.

Una vez que se importan los módulos necesarios, se pueden especificar las tareas. Hay seis tareas registrado,
representado por el código siguiente:

gulp.task("clean:js", function (cb) {


rimraf(paths.concatJsDest, cb);
});

gulp.task("clean:css", function (cb) {


rimraf(paths.concatCssDest, cb);
});

gulp.task("clean", ["clean:js", "clean:css"]);

gulp.task("min:js", function () {
return gulp.src([paths.js, "!" + paths.minJs], { base: "." })
.pipe(concat(paths.concatJsDest))
.pipe(uglify())
.pipe(gulp.dest("."));
});

gulp.task("min:css", function () {
return gulp.src([paths.css, "!" + paths.minCss])
.pipe(concat(paths.concatCssDest))
.pipe(cssmin())
.pipe(gulp.dest("."));
});

gulp.task("min", ["min:js", "min:css"]);

En la tabla siguiente proporciona una explicación de las tareas especificadas en el código anterior:

NOMBRE DE TAREA DESCRIPCIÓN

limpiar: js Una tarea que usa el módulo de eliminación de nodo rimraf


para quitar la versión reducida del archivo site.js.

limpiar: css Una tarea que usa el módulo de eliminación de nodo rimraf
para quitar la versión reducida del archivo site.css.

Limpiar Una tarea que requiera el clean:js tarea, seguido por la


clean:css tarea.
NOMBRE DE TAREA DESCRIPCIÓN

min:js Una tarea que minifica objeto y los concatena todos los
archivos .js dentro de la carpeta para js. El. se excluyen min.js
archivos.

min:CSS Una tarea que minifica objeto y los concatena todos los
archivos .css dentro de la carpeta de css. El. se excluyen
min.css archivos.

min Una tarea que requiera el min:js tarea, seguido por la


min:css tarea.

Ejecución de tareas de manera predeterminada


Si aún no ha creado una nueva aplicación Web, cree un nuevo proyecto de aplicación Web ASP.NET en Visual
Studio.
1. Agregue un nuevo archivo de JavaScript a su proyecto y asígnele el nombre gulpfile.js, a continuación,
copie el código siguiente.
/// <binding Clean='clean' />
"use strict";

var gulp = require("gulp"),


rimraf = require("rimraf"),
concat = require("gulp-concat"),
cssmin = require("gulp-cssmin"),
uglify = require("gulp-uglify");

var paths = {
webroot: "./wwwroot/"
};

paths.js = paths.webroot + "js/**/*.js";


paths.minJs = paths.webroot + "js/**/*.min.js";
paths.css = paths.webroot + "css/**/*.css";
paths.minCss = paths.webroot + "css/**/*.min.css";
paths.concatJsDest = paths.webroot + "js/site.min.js";
paths.concatCssDest = paths.webroot + "css/site.min.css";

gulp.task("clean:js", function (cb) {


rimraf(paths.concatJsDest, cb);
});

gulp.task("clean:css", function (cb) {


rimraf(paths.concatCssDest, cb);
});

gulp.task("clean", ["clean:js", "clean:css"]);

gulp.task("min:js", function () {
return gulp.src([paths.js, "!" + paths.minJs], { base: "." })
.pipe(concat(paths.concatJsDest))
.pipe(uglify())
.pipe(gulp.dest("."));
});

gulp.task("min:css", function () {
return gulp.src([paths.css, "!" + paths.minCss])
.pipe(concat(paths.concatCssDest))
.pipe(cssmin())
.pipe(gulp.dest("."));
});

gulp.task("min", ["min:js", "min:css"]);

2. Abra la package.json archivo (agregar si no existe) y agregue lo siguiente.

{
"devDependencies": {
"gulp": "3.9.1",
"gulp-concat": "2.6.1",
"gulp-cssmin": "0.1.7",
"gulp-uglify": "2.0.1",
"rimraf": "2.6.1"
}
}

3. En el Explorador de soluciones, haga clic en gulpfile.jsy seleccione explorador del ejecutor de tareas.
Explorador del ejecutor de tareas muestra la lista de tareas de Gulp. (Tendrá que hacer clic en el
actualizar botón que aparece a la izquierda del nombre del proyecto.)

IMPORTANT
El explorador del ejecutor de tareas elemento de menú contextual aparece sólo si gulpfile.js está en el directorio
raíz del proyecto.

4. Debajo de la directiva tareas en explorador del ejecutor de tareas, haga clic en limpiay seleccione
ejecutar en el menú emergente.

Explorador del ejecutor de tareas creará una nueva ficha denominada limpia y ejecute la tarea clean tal
y como se define en gulpfile.js.
5. Haga clic en el limpia de tareas, a continuación, seleccione enlaces > antes de compilar.

El antes de compilar enlace configura la tarea clean se ejecute automáticamente antes de cada
compilación del proyecto.
Los enlaces configura con explorador del ejecutor de tareas se almacenan en forma de un comentario en la
parte superior de su gulpfile.js y son eficaces solo en Visual Studio. Es una alternativa que no requiere Visual
Studio configurar la ejecución automática de tareas gulp en su .csproj archivo. Por ejemplo, analizando estos datos
con su .csproj archivo:

<Target Name="MyPreCompileTarget" BeforeTargets="Build">


<Exec Command="gulp clean" />
</Target>

Ahora la tarea clean se ejecuta cuando se ejecuta el proyecto en Visual Studio o desde un símbolo del sistema
mediante la dotnet ejecutar comando (ejecutar npm install primera).

Definir y ejecutar una nueva tarea


Para definir una nueva tarea de Gulp, modificar gulpfile.js.
1. Agregue el siguiente código de JavaScript al final de gulpfile.js:

gulp.task("first", function () {
console.log('first task! <-----');
});

Esta tarea se denomina first , y simplemente muestra una cadena.


2. Guardar gulpfile.js.
3. En el Explorador de soluciones, haga clic en gulpfile.jsy seleccione explorador del ejecutor de tareas.
4. En explorador del ejecutor de tareas, haga clic en primery seleccione ejecutar.
Se muestra el texto de salida. Para ver ejemplos basados en los escenarios comunes, consulte Gulp recetas.

Definir y ejecutar tareas en una serie


Al ejecutar varias tareas, las tareas se ejecutan simultáneamente de forma predeterminada. Sin embargo, si tiene
que ejecutar tareas en un orden específico, debe especificar cada tarea una vez haya finalizado, así como las tareas
que dependen de la finalización de otra tarea.
1. Para definir una serie de tareas que se ejecutan en orden, reemplace la first tareas que agregó
anteriormente en gulpfile.js con lo siguiente:

gulp.task("series:first", function () {
console.log('first task! <-----');
});

gulp.task("series:second", ["series:first"], function () {


console.log('second task! <-----');
});

gulp.task("series", ["series:first", "series:second"], function () {});

Ahora que tiene tres tareas: series:first , series:second , y series . El series:second tarea incluye un
segundo parámetro que especifica una matriz de tareas que se ejecutarán y se complete antes de la
series:second se ejecutará la tarea. Como se especifica en el código anterior, solo la series:first tarea
debe completarse antes de la series:second se ejecutará la tarea.
2. Guardar gulpfile.js.
3. En el Explorador de soluciones, haga clic en gulpfile.js y seleccione explorador del ejecutor de tareas
si no está abierta.
4. En explorador del ejecutor de tareas, haga clic en serie y seleccione ejecutar.
IntelliSense
IntelliSense proporciona la finalización de código, descripciones de parámetros y otras características para
aumentar la productividad y reducir los errores. Tareas de gulp se escriben en JavaScript; por lo tanto, IntelliSense
puede proporcionar asistencia durante el desarrollo. Cuando se trabaja con JavaScript, IntelliSense enumera los
objetos, funciones, propiedades y parámetros que están disponibles según el contexto actual. Seleccione una
opción de codificación de la lista desplegable proporcionada IntelliSense para completar el código.

Para obtener más información acerca de IntelliSense, vea IntelliSense para JavaScript.

Entornos de desarrollo, ensayo y producción


Cuando Gulp se utiliza para optimizar los archivos de cliente de ensayo y producción, se guardan los archivos
procesados en una ubicación local de ensayo y producción. El _Layout.cshtml archivo usa el entorno etiqueta
auxiliar para proporcionar dos versiones diferentes de archivos CSS. Es una versión de los archivos CSS para el
desarrollo y la otra versión está optimizada para ensayo y producción. En Visual Studio 2017 al cambiar la
ASPNETCORE_ENVIRONMENT variable de entorno Production , Visual Studio compilará la aplicación Web y
un vínculo a los archivos CSS minimizados. El marcado siguiente se muestra la entorno etiqueta aplicaciones
auxiliares que contiene las etiquetas de vínculo a la Development CSS archivos y la reducida Staging, Production
archivos CSS.
<environment names="Development">
<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
</environment>
<environment names="Staging,Production">
<script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-2.2.0.min.js"
asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
asp-fallback-test="window.jQuery"
crossorigin="anonymous"
integrity="sha384-K+ctZQ+LL8q6tP7I94W+qzQsfRV2a+AfHIi9k8z8l9ggpc8X+Ytst4yBo/hH+8Fk">
</script>
<script src="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/bootstrap.min.js"
asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.min.js"
asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal"
crossorigin="anonymous"
integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa">
</script>
<script src="~/js/site.min.js" asp-append-version="true"></script>
</environment>

Cambiar entre entornos


Para cambiar entre la compilación para los entornos diferentes, modifique la ASPNETCORE_ENVIRONMENT
valor de la variable de entorno.
1. En explorador del ejecutor de tareas, compruebe que la min tarea se ha establecido para ejecutarse
antes de compilar.
2. En el Explorador de soluciones, haga clic en el nombre del proyecto y seleccione propiedades.
Se muestra la hoja de propiedades de la aplicación Web.
3. Haga clic en la pestaña Depurar.
4. Establezca el valor de la : entorno de hospedaje variable de entorno Production .
5. Presione F5 para ejecutar la aplicación en un explorador.
6. En la ventana del explorador, haga clic en la página y seleccione ver código fuente para ver el código
HTML de la página.
Tenga en cuenta que los vínculos de hoja de estilos apuntan a los archivos CSS reducidos.
7. Cierre el explorador para detener la aplicación Web.
8. En Visual Studio, vuelva a la hoja de propiedades de la aplicación Web y cambiar la : entorno de
hospedaje hacia la variable de entorno Development .
9. Presione F5 volver a ejecutar la aplicación en un explorador.
10. En la ventana del explorador, haga clic en la página y seleccione ver código fuente para ver el código
HTML de la página.
Tenga en cuenta que los vínculos de hoja de estilos apuntan a las versiones de los archivos CSS unminified.
Para obtener más información relacionada con los entornos en ASP.NET Core, vea usar varios entornos.

Detalles de la tarea y módulo


Una tarea de Gulp está registrada con un nombre de función. Puede especificar las dependencias si deben
ejecutar otras tareas antes de la tarea actual. Funciones adicionales que permiten ejecutar y ver las tareas de Gulp,
así como para establecer el origen (src) y de destino (dest) de los archivos que se está modificaciones. Éstas son
las funciones de API Gulp principales:

GULP (FUNCIÓN) SINTAXIS DESCRIPCIÓN

tarea gulp.task(name[, deps], fn) { } El task función crea una tarea. El


name parámetro define el nombre de
la tarea. El deps parámetro contiene
una matriz de tareas se complete antes
de que se ejecuta esta tarea. El fn
parámetro representa una función de
devolución de llamada que realiza las
operaciones de la tarea.

Inspección gulp.watch(glob [, opts], tasks) El watch función supervisa las tareas


{ } de archivos y se ejecuta cuando se
produce un cambio de archivo. El
glob parámetro es un string o
array que determina los archivos que
para ver. El opts parámetro
proporciona viendo opciones de
archivo adicionales.

src gulp.src(globs[, options]) { } El src función proporciona archivos


que coinciden con los valores de glob.
El glob parámetro es un string o
array que determina los archivos que
para leer. El options parámetro
proporciona opciones de archivo
adicionales.

dest gulp.dest(path[, options]) { } El dest función define una ubicación a


la que se pueden escribir archivos. El
path parámetro es una cadena o una
función que determina la carpeta de
destino. El options parámetro es un
objeto que especifica las opciones de
carpeta de salida.

Para obtener información adicional sobre la referencia de API Gulp, consulte Gulp API de documentos.

Gulp recetas
La Comunidad Gulp proporciona Gulp recetas. Estas recetas constan de tareas de Gulp para dirigirse a escenarios
comunes.

Recursos adicionales
Documentación de gulp
Agrupar y minificar en ASP.NET Core
Usar Grunt en ASP.NET Core
Usar Grunt en ASP.NET Core
22/06/2018 • 17 minutes to read • Edit Online

Por Noel arroz


Grunt es un ejecutor de tareas de JavaScript que automatiza la reducción de la secuencia de comandos,
compilación de TypeScript, herramientas de "quitar" de calidad de código, preprocesadores CSS y casi cualquier
tareas repetitivas que debe realizar para admitir el desarrollo de cliente. Grunt es totalmente compatible en Visual
Studio, aunque las plantillas de proyecto ASP.NET utilizan Gulp de forma predeterminada (vea usar Gulp).
Este ejemplo utiliza un proyecto de ASP.NET Core vacío como punto de partida, para mostrar cómo automatizar
el proceso de compilación de cliente desde el principio.
El ejemplo terminado limpia el directorio de implementación de destino, combina los archivos de JavaScript,
comprueba la calidad del código, condensa el contenido del archivo de JavaScript y distribuye a la raíz de la
aplicación web. Usamos los siguientes paquetes:
grunt: paquete de ejecutor de tareas de la Grunt.
limpieza del hogar en grunt: un complemento que quita los archivos o directorios.
grunt-hogar-jshint: un complemento que se revisa la calidad del código JavaScript.
grunt-hogar-concat: un complemento que combina los archivos en un único archivo.
hogar de grunt uglify: un complemento que minifica el objeto de JavaScript para reducir el tamaño.
grunt-hogar-inspección: un complemento que supervisa la actividad de archivo.

Preparación de la aplicación
Para empezar, configure una nueva aplicación web vacía y agregar archivos de ejemplo de TypeScript. Archivos
de typeScript se compilan automáticamente en JavaScript con la configuración de Visual Studio de forma
predeterminada y serán nuestro materias primas para que procese utilizando Grunt.
1. En Visual Studio, cree un nuevo ASP.NET Web Application .
2. En el nuevo proyecto ASP.NET cuadro de diálogo, seleccione el núcleo de ASP.NET vacía plantilla y
haga clic en el botón Aceptar.
3. En el Explorador de soluciones, revise la estructura del proyecto. El \src carpeta incluye vacía wwwroot y
Dependencies nodos.
4. Agregar una nueva carpeta denominada TypeScript al directorio del proyecto.
5. Antes de agregar los archivos, asegúrese de que Visual Studio tiene la opción ' compilar al guardar ' para
comprobar los archivos TypeScript. Vaya a herramientas > opciones > Editor de texto > Typescript >
Proyecto:

6. Haga clic en el TypeScript directorio y seleccione Agregar > nuevo elemento en el menú contextual.
Seleccione el archivo JavaScript de elemento y un nombre al archivo Tastes.ts (tenga en cuenta el *.ts
extensión). Copie la línea de código TypeScript a continuación en el archivo (cuando se guarda, un nuevo
Tastes.js archivo aparecerá con el origen de JavaScript).

enum Tastes { Sweet, Sour, Salty, Bitter }

7. Agregar un segundo archivo en el TypeScript directorio y asígnele el nombre Food.ts . Copie el código
siguiente en el archivo.
class Food {
constructor(name: string, calories: number) {
this._name = name;
this._calories = calories;
}

private _name: string;


get Name() {
return this._name;
}

private _calories: number;


get Calories() {
return this._calories;
}

private _taste: Tastes;


get Taste(): Tastes { return this._taste }
set Taste(value: Tastes) {
this._taste = value;
}
}

Configuración de NPM
A continuación, configure NPM para descargar grunt y tareas grunt.
1. En el Explorador de soluciones, haga clic en el proyecto y seleccione Agregar > nuevo elemento en el
menú contextual. Seleccione el archivo de configuración de NPM item, deje el nombre
predeterminado, package.jsony haga clic en el agregar botón.
2. En el package.json de archivos, en la devDependencies objeto llaves, escriba "grunt". Seleccione grunt de
Intellisense, la lista y presione la tecla ENTRAR. Visual Studio entrecomillar el nombre del paquete grunt y
agregar un signo de dos puntos. A la derecha de los dos puntos, seleccione la versión estable más reciente
del paquete de la parte superior de la lista de Intellisense (presione Ctrl-Space si no aparece Intellisense).

NOTE
Usa NPM control de versiones semántico para organizar las dependencias. Control de versiones semántico, también
conocido como SemVer, identifica los paquetes con el esquema de numeración .. . IntelliSense simplifica el control de
versiones semántico presentando unas cuantas opciones comunes. El elemento superior en la lista de Intellisense
(0.4.5 en el ejemplo anterior) se considera la versión estable más reciente del paquete. El símbolo de intercalación
(^) coincide con la versión principal más reciente y la tilde () co in cide co n la versió n secu n daria más recien te. Consulte la
referencia del analizador de versión NPM semver como guía para la expresividad completa que proporciona SemVer.

3. Agregar más dependencias de carga grunt-hogar -* empaqueta para su limpia, jshint, concat, uglifyy
inspección tal como se muestra en el ejemplo siguiente. Las versiones no es necesario para que coincida
con el ejemplo.
"devDependencies": {
"grunt": "0.4.5",
"grunt-contrib-clean": "0.6.0",
"grunt-contrib-jshint": "0.11.0",
"grunt-contrib-concat": "0.5.1",
"grunt-contrib-uglify": "0.8.0",
"grunt-contrib-watch": "0.6.1"
}

4. Guardar el package.json archivo.


Los paquetes para cada elemento devDependencies van a descargar, junto con los archivos que necesita cada
paquete. Puede encontrar los archivos de paquete en el node_modules directorio habilitando la mostrar todos
los archivos botón en el Explorador de soluciones.

NOTE
Si necesita, puede restaurar manualmente las dependencias en el Explorador de soluciones con el botón secundario en
Dependencies\NPM y seleccionando el restaurar paquetes opción de menú.

Configurar Grunt
Grunt se configura mediante un manifiesto llamado Gruntfile.js que define, carga y registra las tareas que se
pueden ejecutar manualmente o configuradas para ejecutarse automáticamente basándose en eventos en Visual
Studio.
1. Haga clic en el proyecto y seleccione Agregar > nuevo elemento. Seleccione el archivo de
configuración de Grunt opción, deje el nombre predeterminado, Gruntfile.jsy haga clic en el agregar
botón.
El código inicial incluye una definición de módulo y el grunt.initConfig() método. El initConfig() se usa
para establecer las opciones para cada paquete, y el resto del módulo se cargarán y registrar las tareas.

module.exports = function (grunt) {


grunt.initConfig({
});
};
2. Dentro de la initConfig() método, agregue las opciones para la clean tareas tal como se muestra en el
ejemplo Gruntfile.js a continuación. La tarea clean acepta una matriz de cadenas de directorio. Esta tarea
quita archivos wwwroot/lib y quita el directorio temp/todo.

module.exports = function (grunt) {


grunt.initConfig({
clean: ["wwwroot/lib/*", "temp/"],
});
};

3. Debajo del método initConfig(), agregue una llamada a grunt.loadNpmTasks() . Esto hará que la tarea se
puede ejecutar desde Visual Studio.

grunt.loadNpmTasks("grunt-contrib-clean");

4. Guardar Gruntfile.js. El archivo debe ser similar a la captura de pantalla siguiente.

5. Haga clic en Gruntfile.js y seleccione explorador del ejecutor de tareas en el menú contextual. Se abrirá
la ventana del explorador del ejecutor de tareas.

6. Compruebe que clean muestra en tareas en el explorador del ejecutor de tareas.

7. Haga clic en la tarea clean y seleccione ejecutar en el menú contextual. Una ventana de comandos
muestra el progreso de la tarea.
NOTE
No hay ningún archivos o directorios para limpiar todavía. Si lo desea, puede crearlos manualmente en el Explorador
de soluciones y, a continuación, ejecutar la tarea clean como prueba.

8. En el método initConfig(), agregue una entrada para concat con el código siguiente.
El src matriz de propiedades incluyen archivos para combinar en el orden en que deben combinarse. El
dest propiedad asigna la ruta de acceso al archivo combinado que se genera.

concat: {
all: {
src: ['TypeScript/Tastes.js', 'TypeScript/Food.js'],
dest: 'temp/combined.js'
}
},

NOTE
El all propiedad en el código anterior es el nombre de un destino. Los destinos se usan en algunas tareas Grunt
para admitir varios entornos de compilación. Puede ver los destinos integrados mediante Intellisense o asignar la
suya propia.

9. Agregar el jshint tareas mediante el código siguiente.


La utilidad de la calidad del código jshint se ejecuta en cada archivo de JavaScript que se encuentra en el
directorio temporal.

jshint: {
files: ['temp/*.js'],
options: {
'-W069': false,
}
},

NOTE
La opción "-W069" es un error generado por jshint al corchete de cierre de JavaScript usa la sintaxis para asignar
una propiedad en lugar de la notación de puntos, es decir, Tastes["Sweet"] en lugar de Tastes.Sweet . La
opción desactiva la advertencia para permitir que el resto del proceso para continuar.

10. Agregar el uglify tareas mediante el código siguiente.


La tarea minifica objeto el combined.js archivo se encuentra en el directorio temp y crea el archivo de
resultados en wwwroot/lib sigue la convención de nomenclatura estándar <nombre de archivo>. min.js.

uglify: {
all: {
src: ['temp/combined.js'],
dest: 'wwwroot/lib/combined.min.js'
}
},

11. En grunt.loadNpmTasks() de llamada que carga grunt de limpieza del hogar, incluir la misma llamada para
jshint, concat y uglify con el código siguiente.

grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-uglify');

12. Guardar Gruntfile.js. El archivo debe tener un aspecto similar al ejemplo siguiente.

13. Tenga en cuenta que la lista de tareas del explorador de ejecutor de tareas incluye clean , concat , jshint
y uglify tareas. Ejecutar cada tarea en orden y observe los resultados en el Explorador de soluciones.
Cada tarea se debe ejecutar sin errores.

La tarea de concat crea un nuevo combined.js de archivo y lo coloca en el directorio temporal. La tarea de
jshint simplemente se ejecuta y no genera resultados. La tarea de uglify crea un nuevo combined.min.js de
archivo y lo coloca en wwwroot/lib. Al finalizar, la solución debe ser similar a la captura de pantalla
siguiente:

NOTE
Para obtener más información acerca de las opciones para cada paquete, visite https://www.npmjs.com/ y el nombre
del paquete en el cuadro de búsqueda en la página principal de búsqueda. Por ejemplo, puede buscar el paquete
grunt de limpieza del hogar para obtener un vínculo de la documentación que explica todos sus parámetros.

Ahora todos junto


Utilice la Grunt registerTask() método para ejecutar una serie de tareas en un orden determinado. Por ejemplo,
para ejecutar el ejemplo pasos anteriores en el orden correcto -> concat -> jshint -> uglify, agregue el siguiente
código al módulo. El código debe agregarse en el mismo nivel que las llamadas loadNpmTasks(), fuera de
initConfig.

grunt.registerTask("all", ['clean', 'concat', 'jshint', 'uglify']);

La nueva tarea aparece en el explorador del ejecutor de tareas en tareas de Alias. Puede haga y ejecutarla como
lo haría en otras tareas. El all tarea ejecutará clean , concat , jshint y uglify , en orden.

Observación de cambios
Un watch tarea vigila en archivos y directorios. La inspección desencadena tareas automáticamente si detecta los
cambios. Agregue el siguiente código para initConfig para inspeccionar los cambios realizados en *archivos .js en
el directorio TypeScript. Si se modifica un archivo de JavaScript, watch se ejecutará la all tarea.
watch: {
files: ["TypeScript/*.js"],
tasks: ["all"]
}

Agregue una llamada a loadNpmTasks() para mostrar la watch tarea en el explorador del ejecutor de tareas.

grunt.loadNpmTasks('grunt-contrib-watch');

Haga clic en la tarea de inspección de explorador del ejecutor de tareas y seleccione Ejecutar en el menú
contextual. Mostrará la ventana de comandos que muestra la tarea de inspección que se ejecuta un bucle
"esperando..." . Abra uno de los archivos TypeScript, agregue un espacio y, a continuación, guarde el archivo. Esto
desencadena la tarea de inspección y desencadenar las demás tareas que se ejecutan en orden. La captura de
pantalla siguiente muestra una ejemplo de ejecución.

Enlazar a eventos de Visual Studio


A menos que desee iniciar manualmente las tareas de cada vez que funcionan en Visual Studio, puede enlazar
tareas a antes de compilar, después de compilar, limpiar, y Proyecto abierto eventos.
Vamos a enlazar watch para que se ejecute cada vez que abre Visual Studio. En el explorador del ejecutor de
tareas, haga clic en la tarea de inspección y seleccione enlaces > abrir el proyecto en el menú contextual.

Descargar y recargar el proyecto. Cuando se carga el proyecto de nuevo, iniciará la tarea de inspección ejecuta
automáticamente.

Resumen
Grunt es un ejecutor de tareas eficaz que puede utilizarse para automatizar la mayoría de las tareas de
compilación del cliente. Grunt aprovecha NPM para entregar sus paquetes, características y herramientas de
integración con Visual Studio. Explorador del ejecutor de tareas de Visual Studio detecta los cambios en archivos
de configuración y proporciona una interfaz adecuada para ejecutar tareas, ver las tareas en ejecución y enlazar
tareas a eventos de Visual Studio.

Recursos adicionales
Uso de Gulp
Administrar paquetes de cliente con Bower en
ASP.NET Core
22/06/2018 • 10 minutes to read • Edit Online

Por Rick Anderson, Noel arroz, y Scott Addie

IMPORTANT
Mientras se mantiene Bower, sus mantenedores recomienda utilizar una solución distinta. Administrador de bibliotecas de
(LibMan abreviado) es el sistema de administración de contenido estático de cliente nuevo de Visual Studio (Visual Studio
15,8 o posterior). Para obtener más información, consulte Administrador de bibliotecas: Administrador de contenido de
cliente para las aplicaciones web. Bower se admite en Visual Studio a través de la versión 15,5.
Yarn con Webpack es una alternativa para la que instrucciones de migración están disponibles.

Bower llama a sí mismo "Administrador de paquetes para la web". Dentro del ecosistema de. NET, que se llena el
espacio vacío a la izquierda incapacidad de NuGet para entregar archivos de contenido estático. Para los proyectos
de ASP.NET Core, estos archivos estáticos son inherentes a las bibliotecas de cliente como jQuery y arranque. Para
las bibliotecas. NET, seguir usando NuGet Administrador de paquetes.
Proceso de compilación de proyectos nuevos creados con las plantillas de proyecto de ASP.NET Core configurar el
cliente. jQuery y arranque están instalados, y se admite Bower.
Paquetes de cliente se muestran en la bower.json archivo. Configura las plantillas de proyecto de ASP.NET Core
bower.json con jQuery, validación de jQuery y arranque.
En este tutorial, vamos a agregar compatibilidad para fuente Maravilla. Se pueden instalar paquetes de bower con
el administrar paquetes de Bower interfaz de usuario o manualmente en el bower.json archivo.
Instalación mediante la opción administrar paquetes de Bower interfaz de usuario
Crear una nueva aplicación Web de ASP.NET Core con el aplicación Web de ASP.NET Core (.NET Core)
plantilla. Seleccione aplicación Web y sin autenticación.
Haga clic en el proyecto en el Explorador de soluciones y seleccione administrar paquetes de Bower (o
bien en el menú principal, proyecto > administrar paquetes de Bower).
En el Bower: <nombre del proyecto> ventana, haga clic en la ficha "Examinar" y, a continuación, filtre la
lista de paquetes especificando font-awesome en el cuadro de búsqueda:
Confirme que la "guardar los cambios en bower.json" casilla de verificación está activada. Seleccione una
versión de la lista desplegable y haga clic en el instalar botón. El salida ventana muestra los detalles de
instalación.
Instalación manual en bower.json
Abra la bower.json de archivos y agregar "fuente maravillosa" a las dependencias. IntelliSense muestra los
paquetes disponibles. Cuando se selecciona un paquete, se muestran las versiones disponibles. Las siguientes
imágenes anteriores y no coincide con lo que se ve.

Usos de bower control de versiones semántico para organizar las dependencias. Control de versiones semántico,
también conocido como SemVer, identifica los paquetes con el esquema de numeración <principal >.< secundaria
>. <revisión >. IntelliSense simplifica el control de versiones semántico presentando unas cuantas opciones
comunes. El elemento superior en la lista de IntelliSense (4.6.3 en el ejemplo anterior) se considera la versión
estable más reciente del paquete. El símbolo de intercalación (^) coincide con la versión principal más reciente y la
tilde () coincide con la versión secundaria más reciente.
Guardar el bower.json archivo. Visual Studio busca la bower.json archivo para los cambios. Al guardar, el bower
install se ejecuta el comando. Vea la ventana de salida npm/Bower vista para el comando exacto que se ejecuta.
Abra la bowerrc de archivos en bower.json. El directory propiedad está establecida en wwwroot/lib que indica la
ubicación Bower instalará los activos de paquete.

{
"directory": "wwwroot/lib"
}

Puede usar el cuadro de búsqueda en el Explorador de soluciones para buscar y mostrar el paquete de fuente
Maravilla.
Abra la Views\Shared_Layout.cshtml de archivos y agregar el archivo CSS de fuente Maravilla en el entorno de
etiqueta auxiliar para Development . En el Explorador de soluciones, arrastre y coloque fuente awesome.css dentro
de la <environment names="Development"> elemento.

<environment names="Development">
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
<link rel="stylesheet" href="~/css/site.css" />
<link href="~/lib/font-awesome/css/font-awesome.css" rel="stylesheet" />
</environment>

En una aplicación de producción agregaría fuente awesome.min.css a la aplicación auxiliar de etiquetas de entorno
para Staging,Production .
Reemplace el contenido de la Views\Home\About.cshtml archivo Razor con el marcado siguiente:

@{
ViewData["Title"] = "About";
}

<div class="list-group">
<a class="list-group-item" href="#"><i class="fa fa-home fa-fw" aria-hidden="true"></i>&nbsp; Home</a>
<a class="list-group-item" href="#"><i class="fa fa-book fa-fw" aria-hidden="true"></i>&nbsp; Library</a>
<a class="list-group-item" href="#"><i class="fa fa-pencil fa-fw" aria-hidden="true"></i>&nbsp;
Applications</a>
<a class="list-group-item" href="#"><i class="fa fa-cog fa-fw" aria-hidden="true"></i>&nbsp; Settings</a>
</div>

Ejecutar la aplicación y navegue hasta la vista acerca para comprobar el funciona de Maravilla de fuente del
paquete.

Explorar el proceso de compilación de cliente


Plantillas de proyecto de ASP.NET Core mayoría ya están configuradas para usar Bower. En este tutorial siguiente
comienza con un proyecto vacío de ASP.NET Core y agrega cada pieza manualmente, por lo que puede hacerse
una idea de cómo se usa Bower en un proyecto. Puede ver lo que ocurre con la estructura del proyecto y el tiempo
de ejecución como resultado que se realiza cada cambio de configuración.
Los pasos generales para usar el proceso de compilación de cliente con Bower son:
Definir los paquetes utilizados en el proyecto.
Paquetes de referencia desde las páginas web.
Definir paquetes
Una vez que enumerar los paquetes en el bower.json archivo, Visual Studio, descargará. En el ejemplo siguiente se
utiliza Bower para cargar jQuery y arranque a la wwwroot carpeta.
Crear una nueva aplicación Web de ASP.NET Core con el aplicación Web de ASP.NET Core (.NET Core)
plantilla. Seleccione el vacía plantilla de proyecto y haga clic en Aceptar.
En el Explorador de soluciones, haga clic en el proyecto > Agregar nuevo elemento y seleccione archivo
de configuración de Bower. Nota: Una bowerrc también se agrega el archivo.
Abra bower.jsony agregar jquery y arrancar en el dependencies sección. Resultante bower.json archivo
tendrá un aspecto similar al ejemplo siguiente. Las versiones cambiarán con el tiempo y no pueden
coincidir con la imagen siguiente.

{
"name": "asp.net",
"private": true,
"dependencies": {
"jquery": "3.1.1",
"bootstrap": "3.3.7"
}
}

Guardar el bower.json archivo.


Compruebe que el proyecto incluye la arranque y jQuery directorios en wwwroot/lib. Bower utiliza el
bowerrc archivo para instalar los activos en wwwroot/lib.
Nota: La interfaz de usuario "Administrar paquetes de Bower" proporciona una alternativa a modificar el
archivo manualmente.
Habilitar archivos estáticos
Agregar el Microsoft.AspNetCore.StaticFiles paquete NuGet para el proyecto.
Habilitar archivos estáticos que se sirvan con el middleware de archivos estáticos. Agregue una llamada a
UseStaticFiles a la Configure método Startup .

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;

public class Startup


{
public void Configure(IApplicationBuilder app)
{
app.UseStaticFiles();

app.Run(async (context) =>


{
await context.Response.WriteAsync("Hello World!");
});
}
}

Paquetes de referencia
En esta sección, creará una página HTML para comprobar puede tener acceso a los paquetes implementados.
Agregar una nueva página HTML denominada Index.html a la wwwroot carpeta. Nota: Debe agregar el
archivo HTML para la wwwroot carpeta. De forma predeterminada, no se pueda servir contenido estático
fuera wwwroot. Vea archivos estáticos para obtener más información.
Reemplace el contenido de Index.html con el siguiente marcado:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Bower Example</title>
<link href="lib/bootstrap/dist/css/bootstrap.css" rel="stylesheet" />
</head>
<body>
<div class="jumbotron">
<h1>Using the jumbotron style</h1>
<p>
<a class="btn btn-primary btn-lg" role="button">Stateful button</a>
</p>
</div>
<script src="lib/jquery/dist/jquery.js"></script>
<script src="lib/bootstrap/dist/js/bootstrap.js"></script>
<script>
$(".btn").click(function () {
$(this).text('loading')
.delay(1000)
.queue(function () {
$(this).text('reset');
$(this).dequeue();
});
});
</script>
</body>

</html>

Ejecute la aplicación y vaya a http://localhost:<port>/Index.html . O bien, con Index.html abierto, presione


Ctrl+Shift+W . Compruebe que se aplica el estilo jumbotron, el código de jQuery responde cuando se hace
clic en el botón y que el botón de arranque cambia el estado.
Crear sitios maravillosas y capacidad de respuesta
con el arranque y ASP.NET Core
22/06/2018 • 23 minutes to read • Edit Online

Por Steve Smith


Bootstrap actualmente es el marco de trabajo web más popular para desarrollar aplicaciones web con capacidad
de respuesta. Ofrece una serie de características y ventajas que pueden mejorar la experiencia de los usuarios con
el sitio web, si tiene experiencia en el diseño de front-end y de desarrollo o de un experto. Bootstrap se
implementa como un conjunto de archivos CSS y JavaScript y está diseñado para ayudar a su sitio Web o
aplicación escala eficazmente desde teléfonos a tabletas a equipos de escritorio.

Primeros pasos
Hay varias maneras de empezar a trabajar con arranque. Si está iniciando una nueva aplicación web en Visual
Studio, puede elegir la plantilla de inicio predeterminado para ASP.NET Core, en el que se vienen preinstalada
Bootstrap mayúscula:

Adición de arranque a un núcleo de ASP.NET proyecto es simplemente cuestión de agregar a bower.json como
una dependencia:

{
"name": "asp.net",
"private": true,
"dependencies": {
"bootstrap": "3.3.6",
"jquery": "2.2.0",
"jquery-validation": "1.14.0",
"jquery-validation-unobtrusive": "3.2.6"
}
}

Se trata de la manera recomendada de agregar Bootstrap a un proyecto de ASP.NET Core.


También puede instalar bootstrap mediante uno de varios administradores de paquetes, como Bower, npm o
NuGet. En cada caso, el proceso es básicamente el mismo:
Bower

bower install bootstrap

npm

npm install bootstrap

NuGet

Install-Package bootstrap

NOTE
La manera recomendada para instalar las dependencias del lado cliente como programa previo de ASP.NET Core es a través
de Bower (mediante bower.json, tal y como se muestra arriba). Se muestran el uso de npm/NuGet para demostrar cómo
fácilmente arranque puede agregarse a otros tipos de aplicaciones web, incluidas las versiones anteriores de ASP.NET.

Si se están haciendo referencia a sus propias versiones locales de arranque, debe hacer referencia a ellos en todas
las páginas que va a usar. En producción, a que debería hacer referencia arrancar con una CDN. En la plantilla de
sitio ASP.NET de forma predeterminada, el _Layout.cshtml archivo tan similar a la siguiente:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - WebApplication1</title>

<environment names="Development">
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
<link rel="stylesheet" href="~/css/site.css" />
</environment>
<environment names="Staging,Production">
<link rel="stylesheet" href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.6/css/bootstrap.min.css"
asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-
value="absolute" />
<link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" />
</environment>
</head>
<body>
<div class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-
collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a asp-area="" asp-controller="Home" asp-action="Index" class="navbar-
brand">WebApplication1</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<ul class="nav navbar-nav">
<li><a asp-area="" asp-controller="Home" asp-action="Index">Home</a></li>
<li><a asp-area="" asp-controller="Home" asp-action="About">About</a></li>
<li><a asp-area="" asp-controller="Home" asp-action="Contact">Contact</a></li>
</ul>
@await Html.PartialAsync("_LoginPartial")
</div>
</div>
</div>
<div class="container body-content">
@RenderBody()
<hr />
<footer>
<p>&copy; 2016 - WebApplication1</p>
</footer>
</div>

<environment names="Development">
<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
</environment>
<environment names="Staging,Production">
<script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-2.2.0.min.js"
asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
asp-fallback-test="window.jQuery">
</script>
<script src="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.6/bootstrap.min.js"
asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.min.js"
asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal">
</script>
<script src="~/js/site.min.js" asp-append-version="true"></script>
</environment>

@RenderSection("scripts", required: false)


</body>
</html>

NOTE
Si se va a usar cualquiera de los complementos de jQuery de Bootstrap, también debe hacer referencia a jQuery.

Características y plantillas básicas


La plantilla de arranque más básica es muy similar a la _Layout.cshtml archivo que se muestra encima y
simplemente incluye un menú básico para navegación y un lugar para representar el resto de la página.
Navegación básica
La plantilla predeterminada utiliza un conjunto de <div> elementos que se representan una barra de navegación
superior y el cuerpo principal de la página. Si usas HTML5, puede reemplazar la primera <div> etiquetar con un
<nav> etiqueta que se va a obtener el mismo efecto, pero con una semántica más precisa. En este primer <div>
verá hay varios otros. En primer lugar, un <div> con una clase de "contenedor" y, a continuación, en que, más dos
<div> elementos: "encabezado de la barra de navegación" y "barra de navegación se contraen". El elemento div
de encabezado de la barra de navegación incluye un botón que va a aparecer cuando la pantalla está por debajo
de un determinado ancho mínimo, que muestra 3 líneas horizontales (un denominadas "icono de la
hamburguesa"). El icono se representa con puro HTML y CSS; no se requiere ninguna imagen. Éste es el código
que muestra el icono, con cada uno de los etiquetas representar una de las barras blancas:
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>

También incluye el nombre de la aplicación, que aparece en la esquina superior izquierda. El menú de navegación
principal se representa por la <ul> elemento dentro del segundo elemento div e incluye vínculos a casa,
aproximadamente y póngase en contacto con. A continuación el panel de navegación, el cuerpo principal de cada
página se representa en otra <div> , marcada con las clases "contenedor" y "contenido del cuerpo". En el archivo
de _Layout predeterminado simple se muestra aquí, el contenido de la página se representa mediante la vista
específica asociada a la página y, a continuación, una sencilla <footer> se agrega al final de la <div> elemento.
Puede ver cómo integrado acerca de la página aparece con esta plantilla:

La barra de navegación contraída, con el botón de "hamburguesa" en la parte superior derecha, aparece cuando la
ventana cae por debajo de un determinado ancho:
Haga clic en el icono muestra los elementos de menú en un espacio vertical que se desliza hacia abajo desde la
parte superior de la página:

Tipografía y vínculos
Bootstrap configura tipografía básico del sitio, colores y el formato en el archivo CSS del enlace. Este archivo CSS
incluye estilos predeterminados para las tablas, botones, elementos de formulario, imágenes etc. (más). Una
característica útil es el sistema de diseño de cuadrícula, trata más adelante.
Cuadrículas
Una de las características más populares de arranque es su sistema de diseño de cuadrícula. Aplicaciones web
modernas deben evitar usar la <table> etiqueta para el diseño, en su lugar, restringir el uso de este elemento de
datos tabulares reales. En su lugar, columnas y filas se pueden disponer mediante una serie de <div> elementos y
las clases CSS adecuadas. Hay varias ventajas con respecto a este enfoque, incluida la capacidad para ajustar el
diseño de cuadrículas para mostrar verticalmente en pantallas estrechas, como en teléfonos.
Sistema de diseño de cuadrícula del arranque se basa en doce columnas. Se ha elegido este número porque
puede dividirse uniformemente en 1, 2, 3 o 4 columnas y anchos de columna pueden variar a dentro de 1/12 del
ancho vertical de la pantalla. Para empezar a usar el sistema de diseño de cuadrícula, debe comenzar con un
contenedor de <div> y, a continuación, agregue una fila <div> , tal y como se muestra aquí:

<div class="container">
<div class="row">
...
</div>
</div>

A continuación, agregar más <div> elementos para cada columna y especifique el número de columnas que
<div> debe ocupar (fuera de 12 ) como parte de una clase CSS a partir de "col - md-". Por ejemplo, si desea
simplemente tiene dos columnas del mismo tamaño, usaría una clase de "col-md-6" para cada uno de ellos. En
este caso "md" es la abreviatura de "medio" y hace referencia a tamaños de pantalla del equipo de escritorio de
tamaño estándar. Hay cuatro opciones diferentes, que puede elegir entre y cada una se usará para ancho superior
a menos que se reemplaza (por lo que si desea que el diseño debe corregirse, independientemente del ancho de
pantalla, basta con especificar clases extra).

PREFIJO DE LA CLASE CSS NIVEL DE DISPOSITIVO ANCHO

col-xs: Teléfonos < 768px

col-sm - Tabletas > = 768px

col-md - Equipos de escritorio > = 992px

col-lg: Pantallas de escritorio más grandes > = 1200 px

Al especificar dos columnas con "col-md-6" el diseño resultante será dos columnas en las soluciones de escritorio,
pero estas dos columnas se apilan verticalmente cuando se representa en dispositivos más pequeños (o una
ventana del explorador más restringida en un equipo de escritorio), lo que permite a los usuarios ver fácilmente
contenido sin necesidad de desplazarse horizontalmente.
Bootstrap siempre predeterminada será un diseño de columna única, por lo que basta con especificar columnas
cuando desee que más de una columna. La única vez que desearía especificar explícitamente que un <div> sería
ocupan 12 todas las columnas invalidar el comportamiento de un mayor nivel de dispositivo. Al especificar varias
clases de nivel de dispositivo, debe restablecer la presentación de las columnas en ciertos puntos. Agregar un
elemento div clearfix que solo es visible dentro de una ventanilla de cierta puede lograrlo, como se muestra aquí:
En el ejemplo anterior, uno y dos comparten una fila en el diseño "md", mientras que dos y tres comparten una
fila en el diseño de "xs". Sin el clearfix <div> , dos y tres no se muestran correctamente en la vista de "xs" (tenga
en cuenta que se muestra solo uno, cuatro y cinco):
En este ejemplo, una única fila <div> se utilizó, y sigue arranque principalmente ha sido lo correcto en relación
con el diseño y apilado de las columnas. Por lo general, debe especificar una fila <div> para cada fila horizontal
requiere el diseño y, por supuesto puede anidar arranque cuadrículas dentro de uno a otro. Al hacerlo, cada
cuadrícula anidada ocupan 100% del ancho del elemento en el que se coloca, que, a continuación, se subdividen
mediante las clases de columna.
Jumbotron
Si ha usado las plantillas ASP.NET MVC predeterminada en Visual Studio 2012 o 2013, probablemente ha visto el
Jumbotron en acción. Hace referencia a una sección grande de ancho completo de una página que puede usarse
para mostrar una imagen de fondo de gran tamaño, una llamada a la acción, una rotación o elementos similares.
Para agregar una jumbotron a una página, basta con agregar un <div> y asígnele una clase de "jumbotron", a
continuación, coloque un contenedor <div> dentro y agregar su contenido. Podemos ajustar fácilmente el
estándar acerca de la página que se usará un jumbotron para los encabezados principales muestra:

Botones
En la ilustración siguiente se muestran las clases del botón predeterminado y sus colores.
Distintivos
Distintivos hacen referencia a las llamadas pequeño, normalmente numéricos junto a un elemento de navegación.
Puede indicar un número de mensajes o notificaciones en espera, o a la presencia de actualizaciones. Especificar
tales notificaciones es tan sencillo como agregar una <span> que contiene el texto, con una clase de "tarjeta":

Alertas
Puede que necesite mostrar algún tipo de notificación, la alerta o el mensaje de error a los usuarios de la
aplicación. Es donde las clases estándar de alertas son útiles. Hay cuatro niveles de gravedad diferentes con
combinaciones de colores asociados:
Menús y barras de exploración
Nuestro diseño ya incluye una barra de navegación estándar, pero el tema de arranque es compatible con
opciones de estilo adicionales. Podemos realizar fácilmente decidimos para mostrar la barra de navegación
verticalmente en lugar de horizontalmente si lo que se prefiere, así como agregar navegación secundaria
elementos en los menús desplegables. Los menús de navegación sencillo, como bandas de ficha, se generan en la
parte superior de <ul> elementos. Estos pueden crearse muy basta con que se proporcionen con las clases CSS
"nav" y "nav pestañas":
Barras de exploración se generan de forma similar, pero son un poco más complejas. Se inician con un <nav> o
<div> con una clase de "barra de navegación," dentro del cual un elemento div contenedor contiene el resto de
los elementos. Nuestra página incluye una barra de navegación en el encabezado ya; se muestra a continuación
simplemente se expande en el objeto, agrega compatibilidad para un menú desplegable:
Elementos adicionales
También puede utilizarse el tema predeterminado para presentar tablas HTML en un estilo bien formateado,
incluida la compatibilidad con vistas con bandas. Hay etiquetas con estilos que son similares a las de los botones.
Puede crear menús de lista desplegable personalizados que admiten opciones de estilo adicionales más allá de
HTML estándar <select> elemento, junto con barras de exploración como el nuestro sitio de inicio
predeterminada ya está usando. Si necesita una barra de progreso, hay varios estilos para elegir, así como
enumerar los grupos y paneles que incluyen un título y el contenido. Explorar opciones adicionales en el tema de
arranque estándar aquí:
http://getbootstrap.com/examples/theme/
Más temas
Puede ampliar el tema de arranque estándar reemplazando algunos o todos sus CSS, ajustar los colores y estilos
para satisfacer las necesidades de su propia aplicación. Si desea iniciar desde un tema listos para su uso, hay
varias galerías del tema disponibles en línea que están especializados en temas de arranque, como
WrapBootstrap.com (que tiene una gran variedad de temas comerciales) y Bootswatch.com (que ofrece temas
libres). Algunas de las plantillas de pagadas disponibles proporcionan una gran cantidad de funcionalidad sobre el
tema de arranque básica, como compatibilidad enriquecida para los menús administrativos y los paneles con
medidores y gráficos enriquecidos. Un ejemplo de una plantilla de pago popular es Inspinia, actualmente para la
venta de $18, que incluye una plantilla de MVC5 de ASP.NET además de AngularJS y versiones HTML estáticas.
A continuación se muestra una captura de pantalla de ejemplo.

Si desea cambiar el tema del arranque, coloque el bootstrap.css archivo para el tema que desee en el
wwwroot/css carpeta y cambie las referencias en _Layout.cshtml para que lo señale. Cambie los vínculos para
todos los entornos:

<environment names="Development">
<link rel="stylesheet" href="~/css/bootstrap.css" />

<environment names="Staging,Production">
<link rel="stylesheet" href="~/css/bootstrap.min.css" />

Si desea crear su propio panel, puede iniciar desde el ejemplo libre está disponible aquí:
http://getbootstrap.com/examples/dashboard/ .

Componentes
Además de los elementos ya mencionados, arranque incluye compatibilidad para una gran variedad de
componentes de interfaz de usuario integrados.
Glyphicons
Bootstrap incluye conjuntos de iconos de Glyphicons (http://glyphicons.com), con más de 200 iconos libremente
disponibles para su uso dentro de la aplicación web habilitado para arranque. Aquí es sólo una pequeña muestra:
grupos de entrada
Grupos de entrada permiten la agrupación de botones con un elemento de entrada, o de texto adicional que
ofrece al usuario una experiencia más intuitiva:

Rutas de exploración
Rutas son un componente de interfaz de usuario común usado para mostrar su historial reciente o la profundidad
de la jerarquía de navegación de un sitio a un usuario. Agregarlos fácilmente mediante la aplicación de la clase
"ruta de navegación" a cualquier <ol> elemento de lista. Incluye compatibilidad integrada para la paginación
mediante el uso de la clase "paginación" en un <ul> elemento dentro de un <nav> . Agregar presentaciones
incrustados con capacidad de respuesta y vídeo con <iframe> , <embed> , <video> , o <object> elementos de
arranque se estilo automáticamente. Especifique una relación de aspecto determinada mediante el uso de clases
concretas como "Insertar-respondiendo-16by9".

Compatibilidad con JavaScript


Biblioteca de JavaScript del arranque incluye compatibilidad con la API para los componentes incluyen, lo que le
permite controlar su comportamiento mediante programación dentro de la aplicación. Además, bootstrap.js
incluye más de una docena complementos de jQuery personalizados, proporcionar características adicionales,
como las transiciones, cuadros de diálogo modales, desplácese detección (actualicen los estilos en función de
dónde se desplaza el usuario en el documento), comportamiento de contraer, cintas y colocación los menús de la
ventana, por lo que no se desplazan fuera de la pantalla. No hay espacio suficiente para cubrir todos los
complementos de JavaScript que se integran Bootstrap: para aprender más, visite
http://getbootstrap.com/javascript/ .

Resumen
Bootstrap proporciona un marco web que puede usarse para diseñar y aplicar estilo a una gran variedad de sitios
Web y aplicaciones de manera rápida y productiva. Su tipografía básico y estilos proporcionan una apariencia y
funcionamiento agradable que pueden manipular fácilmente mediante la compatibilidad de tema personalizado,
que se puede crear manualmente o adquirió mercado. Admite una serie de componentes web que en el pasado,
tendrías que hacer costosos controles de otros fabricantes para lograr que la ejecución de los estándares web
modernos y abrir.
Menor, Sass y la fuente Maravilla en ASP.NET Core
22/06/2018 • 23 minutes to read • Edit Online

Por Steve Smith


Los usuarios de aplicaciones web tienen cada vez más altas expectativas en cuanto a estilo y experiencia general.
Aplicaciones web modernas aprovechan con frecuencia completas herramientas y marcos de trabajo para definir y
administrar su apariencia y funcionamiento de una manera coherente. Marcos de trabajo como arranque puede ir
mucho a definir un conjunto común de estilos y opciones de diseño para los sitios web. Sin embargo, no trivial
mayoría de los sitios también se beneficiará de poder definir y mantener estilos y archivos (CSS ) de la hoja de
estilos en cascada de forma eficaz, así como tener un acceso sencillo a los iconos de imagen no que ayudan a hacer
más intuitiva interfaz del sitio. Ahí es donde lenguajes y herramientas que admiten menos y Sass, y las bibliotecas
como fuente Maravilla, vienen en.

Idiomas de preprocesador de CSS


Idiomas que se compilan en otros idiomas, con el fin de mejorar la experiencia de trabajar con el lenguaje
subyacente, se conocen como preprocesadores. Hay dos preprocesadores populares de CSS: menos y Sass. Estos
preprocesadores agregar características a CSS, como la posibilidad de variables y reglas anidadas, lo que mejora la
facilidad de mantenimiento de las hojas de estilos de grandes y complejas. CSS como un lenguaje es muy básica,
que carecen de compatibilidad con incluso algo tan sencillo como variables y tiende a crear archivos CSS
repetitivas e inflan. Agregar características de lenguaje de programación real a través de preprocesadores puede
ayudar a reducir la duplicación y proporcionar una mejor organización de reglas de estilo. Visual Studio
proporciona compatibilidad integrada para ambos menor y Sass, así como las extensiones que pueden mejorar la
experiencia de desarrollo cuando se trabaja con estos idiomas.
Como un ejemplo rápido de cómo preprocesadores pueden mejorar la legibilidad y el mantenimiento de
información de estilo, tenga en cuenta este CSS:

.header {
color: black;
font-weight: bold;
font-size: 18px;
font-family: Helvetica, Arial, sans-serif;
}

.small-header {
color: black;
font-weight: bold;
font-size: 14px;
font-family: Helvetica, Arial, sans-serif;
}

Con menor, esto se puede reescribir para eliminar la duplicación, todos con un mixin (denominada así porque
permite "¡mix" propiedades de una clase o conjunto de reglas en el otro):
.header {
color: black;
font-weight: bold;
font-size: 18px;
font-family: Helvetica, Arial, sans-serif;
}

.small-header {
.header;
font-size: 14px;
}

menos
El preprocesador de CSS inferior se ejecuta con Node.js. Para instalar menor, use el Administrador de paquetes de
nodo (npm) desde un símbolo del sistema (-g significa "global"):

npm install -g less

Si está utilizando Visual Studio, puede empezar a trabajar con menor agregando uno o más archivos Less al
proyecto y, a continuación, configurando Gulp (o Grunt) para procesarlos en tiempo de compilación. Agregar un
estilos carpeta al proyecto y, a continuación, agregue un nuevo menos con el nombre de archivo main.less en esta
carpeta.

Una vez agregados, la estructura de carpetas debe tener un aspecto similar al siguiente:
Ahora puede agregar algunos estilos básicos para el archivo, que se compila en CSS e implementa en la carpeta
wwwroot Gulp.
Modificar main.less para incluir el contenido siguiente, que crea una paleta de colores simple desde un único color
base.

@base: #663333;
@background: spin(@base, 180);
@lighter: lighten(spin(@base, 5), 10%);
@lighter2: lighten(spin(@base, 10), 20%);
@darker: darken(spin(@base, -5), 10%);
@darker2: darken(spin(@base, -10), 20%);

body {
background-color:@background;
}
.baseColor {color:@base}
.bgLight {color:@lighter}
.bgLight2 {color:@lighter2}
.bgDark {color:@darker}
.bgDark2 {color:@darker2}

@base y el otro @-prefixed elementos son variables. Cada uno de ellos representa un color. Excepto @base , está
configurados con las funciones de color: aclarar, oscurecer y de número. Aclarar y oscurecer es prácticamente lo
que cabría esperar; número ajusta el matiz de un color en un número de grados (alrededor de la rueda de color). El
procesador de menos es lo suficientemente inteligente como para pasar por alto las variables que no se utilizan,
por lo que para demostrar cómo funcionan estas variables, es necesario utilizarlas en algún lugar. Las clases
.baseColor , etc. se muestran los valores calculados de cada una de las variables en el archivo CSS que se genera.

Primeros pasos
Crear un archivo de configuración de npm (package.json) en la carpeta del proyecto y editarlo para hacer
referencia a gulp y gulp-less :

{
"version": "1.0.0",
"name": "asp.net",
"private": true,
"devDependencies": {
"gulp": "3.9.1",
"gulp-less": "3.3.0"
}
}

Instalar las dependencias en un símbolo del sistema en la carpeta del proyecto, o en Visual Studio el Explorador
de soluciones (dependencias > npm > Restaurar paquetes).
npm install

En la carpeta del proyecto, cree una Gulp archivo de configuración (gulpfile.js) para definir el proceso
automatizado. Agregue una variable en la parte superior del archivo para representar menos y una tarea se ejecute
menor:

var gulp = require("gulp"),


fs = require("fs"),
less = require("gulp-less");

gulp.task("less", function () {
return gulp.src('Styles/main.less')
.pipe(less())
.pipe(gulp.dest('wwwroot/css'));
});

Abra la explorador del ejecutor de tareas (Vista > otras ventanas > Explorador del ejecutor de tareas).
Entre las tareas, verá una nueva tarea denominada less . Es posible que deba actualizar la ventana.
Ejecute la less tarea y obtener un resultado parecido al que se muestra aquí:

El wwwroot/css carpeta contiene ahora un nuevo archivo, main.css:


Abra main.css y verá algo parecido a lo siguiente:

body {
background-color: #336666;
}
.baseColor {
color: #663333;
}
.bgLight {
color: #884a44;
}
.bgLight2 {
color: #aa6355;
}
.bgDark {
color: #442225;
}
.bgDark2 {
color: #221114;
}

Agregar una página HTML sencilla para la wwwroot carpeta y referencia main.css para ver la paleta de colores en
acción.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<link href="css/main.css" rel="stylesheet" />
<title></title>
</head>
<body>
<div>
<div class="baseColor">BaseColor</div>
<div class="bgLight">Light</div>
<div class="bgLight2">Light2</div>
<div class="bgDark">Dark</div>
<div class="bgDark2">Dark2</div>
</div>
</body>
</html>

Puede ver que girar 180 grados en @base se usa para generar @background dio como resultado la rueda de color
opuestas color de @base :
Menor también proporciona compatibilidad para reglas anidadas, así como las consultas de media anidadas. Por
ejemplo, definir las jerarquías anidadas como menús pueden dar lugar a las reglas de CSS detalladas como estos:

nav {
height: 40px;
width: 100%;
}
nav li {
height: 38px;
width: 100px;
}
nav li a:link {
color: #000;
text-decoration: none;
}
nav li a:visited {
text-decoration: none;
color: #CC3333;
}
nav li a:hover {
text-decoration: underline;
font-weight: bold;
}
nav li a:active {
text-decoration: underline;
}

Lo ideal es que todas las reglas de estilo relacionados se colocarán juntos en el archivo CSS, pero en la práctica
que no hay nada aplicar esta regla excepto convención y quizás los comentarios del bloque.
Definir estas mismas reglas que reducen el uso es similar a esto:

nav {
height: 40px;
width: 100%;
li {
height: 38px;
width: 100px;
a {
color: #000;
&:link { text-decoration:none}
&:visited { color: #CC3333; text-decoration:none}
&:hover { text-decoration:underline; font-weight:bold}
&:active {text-decoration:underline}
}
}
}

Tenga en cuenta que en este caso, todos los elementos subordinados de nav están dentro de su ámbito. Ya no hay
ninguna repetición de elementos primarios ( nav , li , a ), y el recuento de líneas total ha disminuido también
(aunque algunos de que es el resultado de que los valores en las mismas líneas en el segundo ejemplo). Puede ser
muy útil, organización ver todas las reglas de un elemento de interfaz de usuario concreto dentro de un ámbito
explícitamente limitado, en este caso desactivar el resto del archivo de llaves.
El & sintaxis es una característica menos selector, con & que representa el elemento primario de selector actual.
Por lo tanto, dentro de la una {...} bloque, & representa un a (etiqueta) y, por tanto, &:link es equivalente a
a:link .

Consultas de medios, muy útiles para crear diseños de capacidad de respuesta, también pueden contribuir con un
alto grado a repeticiones y la complejidad de CSS. Menor permite consultas de medios se anidan dentro de las
clases, por lo que la definición de clase completo no tiene que repetirse dentro de otro nivel superior @media
elementos. Por ejemplo, mostramos CSS para un menú dinámico:

.navigation {
margin-top: 30%;
width: 100%;
}
@media screen and (min-width: 40em) {
.navigation {
margin: 0;
}
}
@media screen and (min-width: 62em) {
.navigation {
width: 960px;
margin: 0;
}
}

Esto se puede definir mejor en menos como:

.navigation {
margin-top: 30%;
width: 100%;
@media screen and (min-width: 40em) {
margin: 0;
}
@media screen and (min-width: 62em) {
width: 960px;
margin: 0;
}
}

Otra característica de menor que ya hemos visto es su compatibilidad para operaciones matemáticas, lo que
permite a los atributos de estilo se construyan a partir de variables definidas previamente. Esto hará que la
actualización relacionados estilos mucho más fácil, ya que se puede modificar la variable de base y todos los
valores dependientes cambian automáticamente.
Archivos CSS, especialmente para sitios de gran tamaño (y especialmente si se usan las consultas de medios),
tienden a alcanzar un tamaño considerable con el tiempo, lo difícil trabajar con ellas. Menos archivos pueden
definirse por separado, a continuación, extrae juntos mediante @import directivas. También se puede usar menor
para importar CSS archivos individuales, también, si lo desea.
Mixins puede aceptar parámetros y menor admite una lógica condicional en forma de protecciones de mixin, que
proporcionan una manera declarativa para definir cuándo determinadas mixins surten efecto. Un uso común de
protecciones de mixin es ajustar los colores en función de la luz u oscuro el color de origen. Dado un mixin que
acepta un parámetro para el color, un guardia mixin puede utilizarse para modificar el mixin en función de ese
color:
.box (@color) when (lightness(@color) >= 50%) {
background-color: #000;
}
.box (@color) when (lightness(@color) < 50%) {
background-color: #FFF;
}
.box (@color) {
color: @color;
}

.feature {
.box (@base);
}

Dada nuestra actual @base valo #663333 , esta secuencia de comandos menos generará el código CSS siguiente:

.feature {
background-color: #FFF;
color: #663333;
}

Menor proporciona una serie de características adicionales, pero esto debe tener una idea de la potencia de este
lenguaje de preprocesamiento.

SASS
SASS es similar a la menor, lo que proporciona compatibilidad para muchas de las mismas características, pero
con una sintaxis ligeramente diferente. Se compila con Ruby, en lugar de JavaScript y, por lo que tiene requisitos
de instalación diferentes. El idioma de Sass original no usa llaves o punto y coma, sino que define ámbito mediante
espacios en blanco y sangría. En la versión 3 de Sass, se introdujo una nueva sintaxis, SCSS ("CSS Sassy"). SCSS
es similar a CSS en que se pasa por alto los espacios en blanco y niveles de sangría y en su lugar utiliza el punto y
coma y llaves.
Para instalar Sass, normalmente se instala por primera vez Ruby (preinstalado en Mac OS ) y, a continuación,
ejecute:

gem install sass

Sin embargo, si está ejecutando Visual Studio, puede empezar a trabajar con Sass prácticamente la misma manera
como lo haría con menor. Abra package.json y agregar el paquete "gulp sass" devDependencies :

"devDependencies": {
"gulp": "3.9.1",
"gulp-less": "3.3.0",
"gulp-sass": "3.1.0"
}

A continuación, modifique gulpfile.js para agregar una variable de sass y una tarea para compilar los archivos Sass
y colocar los resultados en la carpeta wwwroot:
var gulp = require("gulp"),
fs = require("fs"),
less = require("gulp-less"),
sass = require("gulp-sass");

// other content removed

gulp.task("sass", function () {
return gulp.src('Styles/main2.scss')
.pipe(sass())
.pipe(gulp.dest('wwwroot/css'));
});

Ahora puede agregar el archivo Sass main2.scss a la estilos carpeta en la raíz del proyecto:

Abra main2.scss y agregue lo siguiente:

$base: #CC0000;
body {
background-color: $base;
}

Guarde todos los archivos. Ahora, al actualizar explorador del ejecutor de tareas, verá un sass tarea. Ejecútelo
y, en la /wwwroot/css carpeta. Ahora hay un main2.css archivo con este contenido:

body {
background-color: #CC0000;
}

SASS admite el anidamiento prácticamente lo mismo era que menor no, proporcionar beneficios similares. Los
archivos se puede dividir función e incluido mediante la @import directiva:

@import 'anotherfile';

SASS admite mixins así, el uso de la @mixin palabra clave que se va a definirlos y @include para incluirlas, como
en este ejemplo de sass lang.com:

@mixin border-radius($radius) {
-webkit-border-radius: $radius;
-moz-border-radius: $radius;
-ms-border-radius: $radius;
border-radius: $radius;
}

.box { @include border-radius(10px); }

Además de mixins, Sass también admite el concepto de herencia, lo que permite una clase Extender el otro. Es
conceptualmente similar a un mixin, pero da como resultado menos código CSS. Se realiza mediante el @extend
palabra clave. Para probar el mixins, agregue lo siguiente a su main2.scss archivo:

@mixin alert {
border: 1px solid black;
padding: 5px;
color: #333333;
}

.success {
@include alert;
border-color: green;
}

.error {
@include alert;
color: red;
border-color: red;
font-weight:bold;
}

Examine la salida en main2.css después de ejecutar el sass de tareas en explorador del ejecutor de tareas:

.success {
border: 1px solid black;
padding: 5px;
color: #333333;
border-color: green;
}

.error {
border: 1px solid black;
padding: 5px;
color: #333333;
color: red;
border-color: red;
font-weight: bold;
}

Tenga en cuenta que todas las propiedades comunes de la alerta mixin se repiten en cada clase. El mixin realizó un
buen trabajo para ayudar a eliminar la duplicación en tiempo de desarrollo, pero todavía está creando CSS con
una gran cantidad de duplicación en él, lo que produce mayores que los archivos necesarios de CSS - un posible
problema de rendimiento.
Ahora reemplace la alerta mixin con un .alert clase y cambiar @include a @extend (teniendo en cuenta ampliar
.alert , no alert ):
.alert {
border: 1px solid black;
padding: 5px;
color: #333333;
}

.success {
@extend .alert;
border-color: green;
}

.error {
@extend .alert;
color: red;
border-color: red;
font-weight:bold;
}

Ejecute Sass una vez más y examine el código CSS resultante:

.alert, .success, .error {


border: 1px solid black;
padding: 5px;
color: #333333;
}

.success {
border-color: green;
}

.error {
color: red;
border-color: red;
font-weight: bold;
}

Ahora las propiedades se definen únicamente como tantas veces como sea necesario y mejor se genera CSS.
SASS también incluye funciones y operaciones de lógica condicional, similares a un valor inferior. De hecho, las
capacidades de los dos lenguajes son muy similares.

¿Menor o Sass?
Todavía no hay ningún consenso en cuanto a si en general es mejor usar menor o Sass (o incluso si se prefiere la
Sass original o la sintaxis SCSS más reciente en Sass). Probablemente la decisión más importante es utilizar una
de estas herramientas, en lugar de simplemente codificados manualmente los archivos CSS. Una vez que haya
realizado que ambos menor, la decisión y Sass son una buena elección.

Fuente Maravilla
Además de preprocesadores CSS, otro recurso excelente para aplicaciones web modernas de estilo es fantástica de
fuente. Awesome de fuente es un Kit de herramientas que proporciona más de 500 iconos vectoriales escalables
que pueden usarse libremente en sus aplicaciones web. Se diseñó originalmente para trabajar con arranque, pero
no tiene ninguna dependencia en ese marco de trabajo o en cualquier biblioteca de JavaScript.
La manera más fácil empezar a trabajar con fuentes Maravilla consiste en Agregar una referencia a ella, mediante
su ubicación de red (CDN ) de entrega de contenido público:
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css">

También puede agregarlo a su proyecto de Visual Studio, éste se agrega a las "dependencias" en bower.json:

{
"name": "ASP.NET",
"private": true,
"dependencies": {
"bootstrap": "3.0.0",
"jquery": "1.10.2",
"jquery-validation": "1.11.1",
"jquery-validation-unobtrusive": "3.2.2",
"hammer.js": "2.0.4",
"bootstrap-touch-carousel": "0.8.0",
"Font-Awesome": "4.3.0"
}
}

Una vez que tenga una referencia a la fuente Maravilla en una página, puede agregar iconos a la aplicación
aplicando fuente Maravilla clases, normalmente con el prefijo "fa-", a los elementos HTML alineado (como <span>
o <i> ). Por ejemplo, puede agregar iconos a listas simples y los menús con código similar al siguiente:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
<link href="lib/font-awesome/css/font-awesome.css" rel="stylesheet" />
</head>
<body>
<ul class="fa-ul">
<li><i class="fa fa-li fa-home"></i> Home</li>
<li><i class="fa fa-li fa-cog"></i> Settings</li>
</ul>
</body>
</html>

Esto produce el siguiente en el explorador: tenga en cuenta el icono junto a cada elemento:

Puede ver una lista completa de los iconos disponibles aquí:


http://fontawesome.io/icons/

Resumen
Aplicaciones web modernas exigen cada vez más capacidad de respuesta, fluidos diseños que están limpios,
intuitiva y fácil de usar desde una variedad de dispositivos. Administrar la complejidad de las hojas de estilos CSS
necesaria para lograr estos objetivos mejor se realiza mediante un tipo de preprocesador menos o Sass. Además,
kits de herramientas como fuente Maravilla rápidamente proporcionan iconos conocidos para los menús de
navegación textual y experimentan de botones, mejorar la global del usuario de la aplicación.
Agrupación y minifiy activos estáticos en ASP.NET
Core
22/06/2018 • 18 minutes to read • Edit Online

Por Scott Addie


Este artículo explica las ventajas de aplicar la agrupación y minificación, incluido cómo estas características
pueden utilizarse con las aplicaciones web de ASP.NET Core.

¿Qué es agrupar y minificar?


Unión y minificación son dos optimizaciones de rendimiento distintas que puede aplicar en una aplicación web.
Se usan juntos, agrupar y minificar mejoran el rendimiento reduciendo el número de solicitudes de servidor y
reducir el tamaño de los activos estáticos solicitados.
Agrupar y minificar mejoran principalmente el primer tiempo de carga de solicitud de página. Una vez que se ha
solicitado una página web, el explorador almacena en caché los activos estáticos (JavaScript, CSS e imágenes).
Por lo tanto, agrupar y minificar no mejoran el rendimiento cuando se solicita la misma página o páginas, en el
mismo sitio que solicita los activos de la mismos. Si el expira encabezado no está configurado correctamente en
los activos y si no usa la agrupación y minificación, heurística de actualización del explorador Active los recursos
obsoletos después de unos días. Además, el explorador requiere que una solicitud de validación para cada activo.
En este caso, agrupar y minificar proporcionan una mejora del rendimiento incluso después de la primera
solicitud de página.
Cómo agrupar
Cómo agrupar combina varios archivos en un único archivo. Cómo agrupar, reduce el número de solicitudes de
servidor que son necesarios para representar un recurso web, por ejemplo, una página web. Puede crear
cualquier número de paquetes individuales específicamente para CSS, JavaScript, etcetera. Menos archivos
significa menos solicitudes HTTP desde el explorador hasta el servidor o desde el servicio que proporciona la
aplicación. El resultado de la mejora del rendimiento de carga de primera página.
Minificación
Minificación quita caracteres innecesarios de código sin modificar la funcionalidad. El resultado es una reducción
de un tamaño considerable en activos solicitados (por ejemplo, CSS, imágenes y archivos de JavaScript). Los
efectos secundarios comunes de minificación incluyen acortar los nombres de variable a un carácter y quitar los
comentarios y espacios en blanco innecesarios.
Tenga en cuenta la siguiente función de JavaScript:

AddAltToImg = function (imageTagAndImageID, imageContext) {


///<signature>
///<summary> Adds an alt tab to the image
// </summary>
//<param name="imgElement" type="String">The image selector.</param>
//<param name="ContextForImage" type="String">The image context.</param>
///</signature>
var imageElement = $(imageTagAndImageID, imageContext);
imageElement.attr('alt', imageElement.attr('id').replace(/ID/, ''));
}

Minificación reduce la función a la siguiente:


AddAltToImg=function(n,t){var i=$(n,t);i.attr("alt",i.attr("id").replace(/ID/,""))};

Además de quitar los comentarios y espacios en blanco innecesarios, los siguientes nombres de parámetro y la
variable se cambió el nombre como sigue:

ORIGINAL SE CAMBIA EL NOMBRE

imageTagAndImageID t

imageContext a

imageElement r

Impacto de agrupar y minificar


En la tabla siguiente se describe las diferencias entre individualmente carga activos y el uso de agrupación y
minificación:

ACCIÓN CON B/M SIN B/M CAMBIO

Solicitudes de archivos 7 18 157%

KB transferido 156 264.68 70%

Tiempo de carga (ms) 885 2360 167%

Los exploradores son bastante detallados con respecto a los encabezados de solicitud HTTP. El número total de
bytes enviados métrica vio una reducción significativa cuando se agrupa. El tiempo de carga muestra una mejora
considerable, sin embargo, en este ejemplo se ejecutaba localmente. Mayores mejoras de rendimiento se percibe
cuando el uso de agrupación y minificación con activos transfiere a través de una red.

Elegir una estrategia de agrupación y minificación


Las plantillas de proyecto MVC y las páginas de Razor proporcionan una solución para agrupar y minificar que
consta de un archivo de configuración de JSON. Herramientas de terceros, como el Gulp y Grunt corredores de
tareas, realizar las mismas tareas con un poco más compleja. Una herramienta de terceros es una buena elección
cuando el flujo de trabajo de desarrollo requiere un procesamiento más allá de la agrupación y minificación—
como optimización linting e imagen. Mediante el uso de agrupación y minificación de tiempo de diseño, se crean
los archivos reducidos antes de la implementación de la aplicación. Agrupar y minificar antes de la
implementación proporcionan la ventaja de carga reducida del servidor. Sin embargo, es importante reconocer
que agrupación de tiempo de diseño y minificación aumenta la complejidad de la compilación y solo funciona con
archivos estáticos.

Configurar la agrupación y minificación


Las plantillas de proyecto MVC y las páginas de Razor proporcionan una bundleconfig.json archivo de
configuración que define las opciones para cada paquete. De forma predeterminada, se define una configuración
de agrupación única para el código de JavaScript personalizado (wwwroot/js/site.js) y hojas de estilo
(wwwroot/css/site.css) archivos:
[
{
"outputFileName": "wwwroot/css/site.min.css",
"inputFiles": [
"wwwroot/css/site.css"
]
},
{
"outputFileName": "wwwroot/js/site.min.js",
"inputFiles": [
"wwwroot/js/site.js"
],
"minify": {
"enabled": true,
"renameLocals": true
},
"sourceMap": false
}
]

Opciones de configuración incluyen:


outputFileName : El nombre del archivo de paquete para la salida. Puede contener una ruta de acceso relativa
desde la bundleconfig.json archivo. Obligatorio
inputFiles : Una matriz de archivos que se va a agrupar. Estas son las rutas de acceso relativas al archivo de
configuración. opcional, * da como resultado un valor vacío en un archivo de resultados vacío. uso de
comodines patrones son compatibles.
minify : Las opciones de reducción para el tipo de salida. opcional, predeterminado:
minify: { enabled: true }
Opciones de configuración están disponibles por tipo de archivo de salida.
Minificador CSS
Minificador de JavaScript
Minificador de HTML
includeInProject : Marca que indica si se debe agregar los archivos generados al archivo del proyecto.
opcional, predeterminado: false
sourceMap : Marca que indica si se debe generar un mapa de código fuente para el archivo agrupado.
opcional, predeterminado: false
sourceMapRootPath : La ruta de acceso raíz para almacenar el archivo de mapa de código fuente generado.

Ejecución en tiempo de compilación de agrupar y minificar


El BuildBundlerMinifier paquete NuGet permite la ejecución de agrupación y minificación en tiempo de
compilación. Inserta el paquete destinos de MSBuild que ejecutar en la compilación y tiempo de limpieza. El
bundleconfig.json archivo se analiza el proceso de compilación para generar los archivos de salida en función de
la configuración definida.

NOTE
BuildBundlerMinifier pertenece a un proyecto controlado por la Comunidad en GitHub para el que Microsoft no
proporciona compatibilidad con. Deben presentarse problemas aquí.

Visual Studio
CLI de .NET Core
Agregar el BuildBundlerMinifier paquete al proyecto.
Compile el proyecto. Aparecerá el siguiente mensaje en la ventana de salida:

1>------ Build started: Project: BuildBundlerMinifierApp, Configuration: Debug Any CPU ------
1>
1>Bundler: Begin processing bundleconfig.json
1> Minified wwwroot/css/site.min.css
1> Minified wwwroot/js/site.min.js
1>Bundler: Done processing bundleconfig.json
1>BuildBundlerMinifierApp -> C:\BuildBundlerMinifierApp\bin\Debug\netcoreapp2.0\BuildBundlerMinifierApp.dll
========== Build: 1 succeeded, 0 failed, 0 up-to-date, 0 skipped ==========

Limpie el proyecto. Aparecerá el siguiente mensaje en la ventana de salida:

1>------ Clean started: Project: BuildBundlerMinifierApp, Configuration: Debug Any CPU ------
1>
1>Bundler: Cleaning output from bundleconfig.json
1>Bundler: Done cleaning output file from bundleconfig.json
========== Clean: 1 succeeded, 0 failed, 0 skipped ==========

Ejecución ad hoc de agrupar y minificar


Es posible ejecutar las tareas de agrupación y minificación de manera ad hoc, sin compilar el proyecto. Agregar el
BundlerMinifier.Core paquete NuGet para el proyecto:

<DotNetCliToolReference Include="BundlerMinifier.Core" Version="2.6.362" />

NOTE
BundlerMinifier.Core pertenece a un proyecto controlado por la Comunidad en GitHub para el que Microsoft no
proporciona compatibilidad con. Deben presentarse problemas aquí.

Este paquete extiende la CLI de núcleo de .NET para incluir la dotnet agrupación herramienta. En la ventana de
consola de administrador de paquetes (PMC ) o en un shell de comandos, se puede ejecutar el comando siguiente:

dotnet bundle

IMPORTANT
Administrador de paquetes de NuGet agrega las dependencias en el archivo *.csproj como <PackageReference /> nodos.
El dotnet bundle comando está registrado con la CLI de .NET Core solo cuando un <DotNetCliToolReference /> nodo
se utiliza. Modifique el archivo *.csproj según corresponda.

Agregar archivos al flujo de trabajo


Considere un ejemplo en el que más custom.css archivo se agrega similar a lo siguiente:
.about, [role=main], [role=complementary] {
margin-top: 60px;
}

footer {
margin-top: 10px;
}

A minificar custom.css y agrupar con site.css en un site.min.css , agregue la ruta de acceso relativa a
bundleconfig.json:

[
{
"outputFileName": "wwwroot/css/site.min.css",
"inputFiles": [
"wwwroot/css/site.css",
"wwwroot/css/custom.css"
]
},
{
"outputFileName": "wwwroot/js/site.min.js",
"inputFiles": [
"wwwroot/js/site.js"
],
"minify": {
"enabled": true,
"renameLocals": true
},
"sourceMap": false
}
]

NOTE
Como alternativa, podría utilizar el siguiente patrón de uso de comodines en:

"inputFiles": ["wwwroot/**/*(*.css|!(*.min.css)"]

Este patrón de uso de comodines coincide con todos los archivos CSS y excluye el patrón de archivo reducida.

Compile la aplicación. Abra site.min.css y observe el contenido de custom.css se anexa al final del archivo.

Agrupar y minificar basado en el entorno


Como práctica recomendada, los archivos integrados y reducidos de la aplicación deben usarse en un entorno de
producción. Durante el desarrollo, los archivos originales se realizan para una depuración más sencilla de la
aplicación.
Especificar qué archivos desea incluir en las páginas mediante el auxiliar de etiquetas de entorno en las vistas. La
aplicación auxiliar de etiquetas de entorno solo representa su contenido cuando se ejecuta en específico entornos.
El siguiente environment etiqueta representa los archivos sin procesar de CSS cuando se ejecuta en el
Development entorno:
ASP.NET Core 2.x
ASP.NET Core 1.x
<environment include="Development">
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
<link rel="stylesheet" href="~/css/site.css" />
</environment>

El siguiente environment etiqueta representa los archivos CSS agrupados y reducidos cuando se ejecuta en un
entorno distinto de Development . Por ejemplo, que se ejecutan Production o Staging desencadena el
procesamiento de estas hojas de estilos:
ASP.NET Core 2.x
ASP.NET Core 1.x

<environment exclude="Development">
<link rel="stylesheet" href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.min.css"
asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-
value="absolute" />
<link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" />
</environment>

Consumir bundleconfig.json desde Gulp


Hay casos en los que el flujo de trabajo de una aplicación agrupar y minificar requiere un procesamiento
adicional. Algunos ejemplos son la optimización de la imagen, la desactivación de caché y el procesamiento del
recurso de red CDN. Para satisfacer estos requisitos, que puede convertir el flujo de trabajo de agrupación y
minificación para usar Gulp.
Utilizar la extensión de paquete de instalación & Minificador
Visual Studio paquete de instalación & Minificador extensión controla la conversión a Gulp.

NOTE
La extensión del paquete de instalación & Minificador pertenece a un proyecto controlado por la Comunidad en GitHub
para el que Microsoft no proporciona compatibilidad con. Deben presentarse problemas aquí.

Haga clic en el bundleconfig.json un archivo en el Explorador de soluciones y seleccione paquete de instalación


& Minificador > convertir a Gulp... :

El gulpfile.js y package.json archivos se agregan al proyecto. La compatibilidad con npm paquetes incluidos en el
package.json del archivo devDependencies sección están instalados.
Ejecute el siguiente comando en la ventana PMC para instalar la CLI Gulp como una dependencia global:
npm i -g gulp-cli

El gulpfile.js lecturas de archivos la bundleconfig.json archivo de entradas, salidas y configuración.

"use strict";

var gulp = require("gulp"),


concat = require("gulp-concat"),
cssmin = require("gulp-cssmin"),
htmlmin = require("gulp-htmlmin"),
uglify = require("gulp-uglify"),
merge = require("merge-stream"),
del = require("del"),
bundleconfig = require("./bundleconfig.json");

// Code omitted for brevity

Convertir manualmente
Si Visual Studio o la extensión del paquete de instalación & Minificador no está disponible, convertir
manualmente.
Agregar un package.json archivo, con el siguiente devDependencies , a la raíz del proyecto:

"devDependencies": {
"del": "^3.0.0",
"gulp": "^3.9.1",
"gulp-concat": "^2.6.1",
"gulp-cssmin": "^0.2.0",
"gulp-htmlmin": "^3.0.0",
"gulp-uglify": "^3.0.0",
"merge-stream": "^1.0.1"
}

Instalar las dependencias ejecutando el siguiente comando en el mismo nivel que package.json:

npm i

Instale la CLI Gulp como una dependencia global:

npm i -g gulp-cli

Copia la gulpfile.js por debajo del archivo a la raíz del proyecto:

"use strict";

var gulp = require("gulp"),


concat = require("gulp-concat"),
cssmin = require("gulp-cssmin"),
htmlmin = require("gulp-htmlmin"),
uglify = require("gulp-uglify"),
merge = require("merge-stream"),
del = require("del"),
bundleconfig = require("./bundleconfig.json");

var regex = {
css: /\.css$/,
html: /\.(html|htm)$/,
js: /\.js$/
js: /\.js$/
};

gulp.task("min", ["min:js", "min:css", "min:html"]);

gulp.task("min:js", function () {
var tasks = getBundles(regex.js).map(function (bundle) {
return gulp.src(bundle.inputFiles, { base: "." })
.pipe(concat(bundle.outputFileName))
.pipe(uglify())
.pipe(gulp.dest("."));
});
return merge(tasks);
});

gulp.task("min:css", function () {
var tasks = getBundles(regex.css).map(function (bundle) {
return gulp.src(bundle.inputFiles, { base: "." })
.pipe(concat(bundle.outputFileName))
.pipe(cssmin())
.pipe(gulp.dest("."));
});
return merge(tasks);
});

gulp.task("min:html", function () {
var tasks = getBundles(regex.html).map(function (bundle) {
return gulp.src(bundle.inputFiles, { base: "." })
.pipe(concat(bundle.outputFileName))
.pipe(htmlmin({ collapseWhitespace: true, minifyCSS: true, minifyJS: true }))
.pipe(gulp.dest("."));
});
return merge(tasks);
});

gulp.task("clean", function () {
var files = bundleconfig.map(function (bundle) {
return bundle.outputFileName;
});

return del(files);
});

gulp.task("watch", function () {
getBundles(regex.js).forEach(function (bundle) {
gulp.watch(bundle.inputFiles, ["min:js"]);
});

getBundles(regex.css).forEach(function (bundle) {
gulp.watch(bundle.inputFiles, ["min:css"]);
});

getBundles(regex.html).forEach(function (bundle) {
gulp.watch(bundle.inputFiles, ["min:html"]);
});
});

function getBundles(regexPattern) {
return bundleconfig.filter(function (bundle) {
return regexPattern.test(bundle.outputFileName);
});
}

Ejecutar tareas de Gulp


Para desencadenar la tarea de preparación de minificación Gulp antes de que el proyecto se compila en Visual
Studio, agregue las siguientes destino de MSBuild al archivo *.csproj:
<Target Name="MyPreCompileTarget" BeforeTargets="Build">
<Exec Command="gulp min" />
</Target>

En este ejemplo, las tareas se definen en el MyPreCompileTarget destino ejecutar antes predefinido Build destino.
Aparecerá un resultado similar al siguiente en la ventana de salida de Visual Studio:

1>------ Build started: Project: BuildBundlerMinifierApp, Configuration: Debug Any CPU ------
1>BuildBundlerMinifierApp -> C:\BuildBundlerMinifierApp\bin\Debug\netcoreapp2.0\BuildBundlerMinifierApp.dll
1>[14:17:49] Using gulpfile C:\BuildBundlerMinifierApp\gulpfile.js
1>[14:17:49] Starting 'min:js'...
1>[14:17:49] Starting 'min:css'...
1>[14:17:49] Starting 'min:html'...
1>[14:17:49] Finished 'min:js' after 83 ms
1>[14:17:49] Finished 'min:css' after 88 ms
========== Build: 1 succeeded, 0 failed, 0 up-to-date, 0 skipped ==========

También se puede usar el explorador del ejecutor de tareas de Visual Studio para enlazar Gulp tareas a eventos
específicos de Visual Studio. Vea ejecutando tareas predeterminadas para obtener instrucciones sobre cómo
hacer que.

Recursos adicionales
Uso de Gulp
Uso de Grunt
Uso de varios entornos
Aplicaciones auxiliares de etiquetas
Vínculo de explorador en ASP.NET Core
22/06/2018 • 8 minutes to read • Edit Online

Por Nicolò Carandini, Mike Wasson, y Tom Dykstra


Vínculo de explorador es una característica de Visual Studio que crea un canal de comunicación entre el entorno de
desarrollo y uno o varios exploradores web. Puede usar el vínculo de explorador para actualizar la aplicación web
en varios exploradores a la vez, lo que resulta útil para las pruebas de varios exploradores.

Programa de instalación de vínculo de explorador


ASP.NET Core 2.x
ASP.NET Core 1.x
La versión 2.0 de ASP.NET Core aplicación Web, vacía, y API Web plantilla proyectos utilizan la
Microsoft.AspNetCore.All metapackage , que contiene una referencia de paquete para
Microsoft.VisualStudio.Web.BrowserLink. Por tanto, se usan los Microsoft.AspNetCore.All metapackage no
requiere ninguna acción adicional para que estén disponibles para su uso vínculo de explorador.
Al convertir un proyecto de ASP.NET Core 2.0 a ASP.NET Core 2.1 y realizar la transición a la
Microsoft.AspNetCore.App metapackage, debe instalar la Microsoft.VisualStudio.Web.BrowserLink paquete
manualmente para la funcionalidad de BrowserLink.
Configuración
En el Configure método de la Startup.cs archivo:

app.UseBrowserLink();

Normalmente es el código dentro de un if bloque que solo habilita el vínculo de explorador en el entorno de
desarrollo, como se muestra aquí:

if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}

Para obtener más información, consulte Uso de varios entornos.

Cómo usar el vínculo de explorador


Cuando hay un proyecto de ASP.NET Core abierta, Visual Studio muestra el control de barra de herramientas de
vínculo de explorador junto a la destino de depuración control de barra de herramientas:
Desde el control de barra de herramientas de vínculo de explorador, hacer lo siguiente:
Actualizar la aplicación web en varios exploradores a la vez.
Abra la panel de vínculos de explorador.
Habilitar o deshabilitar vínculo de explorador. Nota: El vínculo de explorador está deshabilitada de forma
predeterminada en Visual Studio 2017 (15.3).
Habilitar o deshabilitar sincronización automática de CSS.

NOTE
Algunos complementos de Visual Studio, sobre todo 2015 de paquete de extensión de Web y 2017 de paquete de extensión
de Web, ofrecen una funcionalidad extendida de vínculo de explorador, pero algunas de las características adicionales no
funcionan con ASP. Proyectos de red principal.

Actualizar la aplicación web en varios exploradores a la vez


Para elegir un explorador web único para iniciar al iniciar el proyecto, utilice el menú desplegable de la destino de
depuración control de barra de herramientas:

Para abrir varios exploradores a la vez, elija examinar con... en la misma lista desplegable. Mantenga presionada
la tecla CTRL para seleccionar los exploradores que desee y, a continuación, haga clic en examinar:
Esta es una captura de pantalla que muestra Visual Studio con la vista de índice abierto y dos exploradores
abiertos:

Mantenga el mouse sobre el control de barra de herramientas de vínculo de explorador para ver los exploradores
que están conectados al proyecto:
Cambie la vista de índice y se actualizan todos los exploradores conectados al hacer clic en el botón de
actualización de vínculo de explorador:

Vínculo de explorador también funciona con exploradores que inicie desde fuera de Visual Studio y navegue a la
dirección URL de la aplicación.
El panel de vínculos de explorador
Abrir el panel de vínculo de explorador desde el menú para administrar la conexión con exploradores abiertos
desplegable de vínculo de explorador:
Si no hay ningún explorador está conectado, puede iniciar una sesión de depuración no seleccionando la ver en el
explorador vínculo:

En caso contrario, se muestran los exploradores conectados con la ruta de acceso a la página que se muestra cada
explorador:
Si lo desea, puede hacer clic en un nombre de lista del explorador para actualizar esa única del explorador.
Habilitar o deshabilitar el vínculo de explorador
Al volver a habilitar vínculo de explorador después de deshabilitarlo, debe actualizar los exploradores para volver a
conectarlos.
Habilitar o deshabilitar la sincronización automática de CSS
Cuando se habilita la sincronización automática de CSS, exploradores conectados se actualizan automáticamente
cuando se realiza cualquier cambio a los archivos CSS.

¿Cómo funciona?
Vínculo de explorador usa SignalR para crear un canal de comunicación entre Visual Studio y el explorador.
Cuando se habilita el vínculo de explorador, Visual Studio actúa como un servidor de SignalR que varios clientes
(exploradores) pueden conectarse a. Vínculo de explorador también registra un componente de middleware en la
canalización de solicitudes ASP.NET. Este componente inserta especial <script> referencias en cada solicitud de
página desde el servidor. Puede ver las referencias de script seleccionando ver código fuente en el explorador y
el desplazamiento hasta el final de la <body> etiquetar contenido:

<!-- Visual Studio Browser Link -->


<script type="application/json" id="__browserLink_initializationData">
{"requestId":"a717d5a07c1741949a7cefd6fa2bad08","requestMappingFromServer":false}
</script>
<script type="text/javascript" src="http://localhost:54139/b6e36e429d034f578ebccd6a79bf19bf/browserLink"
async="async"></script>
<!-- End Browser Link -->
</body>

No modifican los archivos de origen. El componente de middleware inserta dinámicamente las referencias de
script.
Dado que el código del lado del explorador es JavaScript todos, funciona en todos los exploradores compatibles
con SignalR sin necesidad de un complemento de explorador.
Usar JavaScriptServices para crear aplicaciones de
una página de ASP.NET Core
22/06/2018 • 22 minutes to read • Edit Online

Por Scott Addie y Fiyaz Hasan


Una aplicación de página única (SPA) es un tipo conocido de aplicación web debido a su experiencia de usuario
completa e inherente. La integración de marcos o bibliotecas SPA del lado cliente, como Angular o React, con
marcos del lado servidor como ASP.NET Core puede ser difícil. JavaScriptServices se desarrolló para reducir la
fricción en el proceso de integración. Permite el funcionamiento sin problemas entre los distintos componentes
tecnológicos del lado servidor y cliente.
Vea o descargue el código de ejemplo (cómo descargarlo)

¿Qué es JavaScriptServices?
JavaScriptServices es un conjunto de tecnologías de cliente para ASP.NET Core. Su objetivo consiste en colocar
ASP.NET Core como plataforma de servidor preferido de los desarrolladores para compilar SPAs.
JavaScriptServices consta de tres paquetes de NuGet distintos:
Microsoft.AspNetCore.NodeServices (NodeServices)
Microsoft.AspNetCore.SpaServices (SpaServices)
Microsoft.AspNetCore.SpaTemplates (SpaTemplates)
Estos paquetes son útiles si se:
Ejecutar JavaScript en el servidor
Usa un marco o biblioteca de SPA
Compilar recursos de lado cliente con Webpack
Gran parte de la atención en este artículo se coloca sobre cómo usar el paquete SpaServices.

¿Qué es SpaServices?
SpaServices se creó para colocar ASP.NET Core como la plataforma del lado servidor preferida de los
desarrolladores para compilar las SPA. SpaServices no es necesario para desarrollar SPA con ASP.NET Core y no
le obliga a usar un marco de cliente en particular.
SpaServices ofrece una infraestructura útil como:
Procesamiento previo de servidor
Middleware de desarrollo Webpack
Sustitución del módulo de acceso rápido
Aplicaciones auxiliares de enrutamientos
Colectivamente, estos componentes de infraestructura mejoran el flujo de trabajo de desarrollo y la experiencia en
tiempo de ejecución. Los componentes pueden adoptar individualmente.

Requisitos previos para usar SpaServices


Para trabajar con SpaServices, instalar lo siguiente:
Node.js (versión 6 o posterior) con npm
Para comprobar que estos componentes se instalan y se encuentra, ejecute lo siguiente desde la línea
de comandos:

node -v && npm -v

Nota: Si va a implementar en un sitio web de Azure, no es necesario hacer nada aquí — Node.js está instalada y
disponible en los entornos de servidor.
.NET Core SDK 2.0 or later
Si está en Windows con Visual Studio de 2017, está instalado el SDK seleccionando la .NET Core el
desarrollo multiplataforma carga de trabajo.
Microsoft.AspNetCore.SpaServices paquete de NuGet

Procesamiento previo de servidor


Una aplicación universal (también conocida como isomorfa) es una aplicación de JavaScript capaz de ejecutarse
tanto en el servidor como en el cliente. Angular, React y otros marcos populares proporcionan una plataforma
universal para el desarrollo de este estilo de aplicaciones. La idea es representar primero los componentes del
marco en el servidor a través de Node.js y, después, delegar el resto de la ejecución al cliente.
ASP.NET Core aplicaciones auxiliares de etiquetas proporcionada por SpaServices simplificar la implementación
de procesamiento previo de servidor mediante la invocación de las funciones de JavaScript en el servidor.
Requisitos previos
Instale el software siguiente:
ASPNET-procesamiento previo npm paquete:

npm i -S aspnet-prerendering

Configuración
Las aplicaciones auxiliares de etiquetas se hacen reconocibles a través de registro del espacio de nombres en el
proyecto _ViewImports.cshtml archivo:

@using SpaServicesSampleApp
@addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers"
@addTagHelper "*, Microsoft.AspNetCore.SpaServices"

Estas aplicaciones auxiliares de etiquetas abstraer los detalles de comunicarse directamente con las API de bajo
nivel mediante el aprovechamiento de una sintaxis similar a HTML dentro de la vista Razor:

<app asp-prerender-module="ClientApp/dist/main-server">Loading...</app>

El asp-prerender-module auxiliar de etiquetas


El asp-prerender-module se ejecuta la aplicación auxiliar de etiquetas, usado en el ejemplo de código anterior,
ClientApp/dist/main-server.js en el servidor a través de Node.js. Para no complicarlo, main server.js archivo es un
artefacto de la tarea de transpilation TypeScript y JavaScript en el Webpack proceso de compilación. Webpack
define un alias de punto de entrada de main-server ; y cruce seguro del gráfico de dependencia para este alias
comienza en el ClientApp/arranque-server.ts archivo:
entry: { 'main-server': './ClientApp/boot-server.ts' },

En el siguiente ejemplo Angular, el ClientApp/arranque-server.ts archivo utiliza el createServerRenderer función y


RenderResult el tipo de la aspnet-prerendering paquete de npm para configurar la representación del servidor a
través de Node.js. El marcado HTML destinado para la representación del lado servidor se pasa a una llamada de
función de la resolución, que se incluye en JavaScript fuertemente tipado Promise objeto. El Promise es de
importancia del objeto que proporciona asincrónicamente el marcado HTML a la página de inyección de código en
el elemento del DOM al marcador de posición.

import { createServerRenderer, RenderResult } from 'aspnet-prerendering';

export default createServerRenderer(params => {


const providers = [
{ provide: INITIAL_CONFIG, useValue: { document: '<app></app>', url: params.url } },
{ provide: 'ORIGIN_URL', useValue: params.origin }
];

return platformDynamicServer(providers).bootstrapModule(AppModule).then(moduleRef => {


const appRef = moduleRef.injector.get(ApplicationRef);
const state = moduleRef.injector.get(PlatformState);
const zone = moduleRef.injector.get(NgZone);

return new Promise<RenderResult>((resolve, reject) => {


zone.onError.subscribe(errorInfo => reject(errorInfo));
appRef.isStable.first(isStable => isStable).subscribe(() => {
// Because 'onStable' fires before 'onError', we have to delay slightly before
// completing the request in case there's an error to report
setImmediate(() => {
resolve({
html: state.renderToString()
});
moduleRef.destroy();
});
});
});
});
});

El asp-prerender-data auxiliar de etiquetas


Cuando se acopla con el asp-prerender-module aplicación auxiliar de etiqueta, la asp-prerender-data etiqueta
auxiliar puede utilizarse para pasar información contextual de la vista de Razor en el código de JavaScript del lado
servidor. Por ejemplo, el siguiente marcado pasa los datos de usuario en el main-server módulo:

<app asp-prerender-module="ClientApp/dist/main-server"
asp-prerender-data='new {
UserName = "John Doe"
}'>Loading...</app>

Los datos recibidos UserName argumento se serializa utilizando el serializador JSON integrado y se almacena en la
params.data objeto. En el siguiente ejemplo Angular, los datos se utilizan para construir un saludo personalizado
dentro de un h1 elemento:
import { createServerRenderer, RenderResult } from 'aspnet-prerendering';

export default createServerRenderer(params => {


const providers = [
{ provide: INITIAL_CONFIG, useValue: { document: '<app></app>', url: params.url } },
{ provide: 'ORIGIN_URL', useValue: params.origin }
];

return platformDynamicServer(providers).bootstrapModule(AppModule).then(moduleRef => {


const appRef = moduleRef.injector.get(ApplicationRef);
const state = moduleRef.injector.get(PlatformState);
const zone = moduleRef.injector.get(NgZone);

return new Promise<RenderResult>((resolve, reject) => {


const result = `<h1>Hello, ${params.data.userName}</h1>`;

zone.onError.subscribe(errorInfo => reject(errorInfo));


appRef.isStable.first(isStable => isStable).subscribe(() => {
// Because 'onStable' fires before 'onError', we have to delay slightly before
// completing the request in case there's an error to report
setImmediate(() => {
resolve({
html: result
});
moduleRef.destroy();
});
});
});
});
});

Nota: Los nombres de propiedad pasados en aplicaciones auxiliares de etiquetas se representan con PascalCase
notación. Esto contrasta con JavaScript, donde los mismos nombres de propiedad se representan con camelCase.
La configuración predeterminada de la serialización de JSON es responsable de esta diferencia.
Para ampliar el ejemplo de código anterior, datos pueden pasarse desde el servidor a la vista por hidratación el
globals propiedad proporcionada para el resolve función:
import { createServerRenderer, RenderResult } from 'aspnet-prerendering';

export default createServerRenderer(params => {


const providers = [
{ provide: INITIAL_CONFIG, useValue: { document: '<app></app>', url: params.url } },
{ provide: 'ORIGIN_URL', useValue: params.origin }
];

return platformDynamicServer(providers).bootstrapModule(AppModule).then(moduleRef => {


const appRef = moduleRef.injector.get(ApplicationRef);
const state = moduleRef.injector.get(PlatformState);
const zone = moduleRef.injector.get(NgZone);

return new Promise<RenderResult>((resolve, reject) => {


const result = `<h1>Hello, ${params.data.userName}</h1>`;

zone.onError.subscribe(errorInfo => reject(errorInfo));


appRef.isStable.first(isStable => isStable).subscribe(() => {
// Because 'onStable' fires before 'onError', we have to delay slightly before
// completing the request in case there's an error to report
setImmediate(() => {
resolve({
html: result,
globals: {
postList: [
'Introduction to ASP.NET Core',
'Making apps with Angular and ASP.NET Core'
]
}
});
moduleRef.destroy();
});
});
});
});
});

El postList matriz definida dentro de la globals objeto se adjunta en el explorador global window objeto. Esta
activación variables en ámbito global elimina la duplicación de esfuerzos, especialmente ya que pertenece a la
carga de los mismos datos una vez en el servidor y de nuevo en el cliente.

Middleware de desarrollo Webpack


Middleware de desarrollo Webpack incorpora un flujo de trabajo de desarrollo mejorado mediante el cual
Webpack crea recursos a petición. El middleware automáticamente compila y sirve de recursos del cliente cuando
se vuelve a cargar una página en el explorador. El método alternativo consiste en invocar manualmente Webpack a
través de script de compilación del proyecto npm cuando cambia una dependencia de otro fabricante o el código
personalizado. Un npm Generar script en el package.json archivo se muestra en el ejemplo siguiente:

"build": "npm run build:vendor && npm run build:custom",

Requisitos previos
Instale el software siguiente:
ASPNET webpack npm paquete:

npm i -D aspnet-webpack

Configuración
Middleware de desarrollo Webpack está registrado en la canalización de solicitudes HTTP mediante el siguiente
código en el Startup.cs del archivo Configure método:

if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseWebpackDevMiddleware();
}
else
{
app.UseExceptionHandler("/Home/Error");
}

// Call UseWebpackDevMiddleware before UseStaticFiles


app.UseStaticFiles();

El UseWebpackDevMiddleware método de extensión se debe llamar antes registrar archivos estáticos hospedaje a
través de la UseStaticFiles método de extensión. Por motivos de seguridad, registre el middleware solo cuando la
aplicación se ejecuta en modo de desarrollo.
El webpack.config.js del archivo output.publicPath propiedad indica el middleware para observar el dist carpeta
cambios:

module.exports = (env) => {


output: {
filename: '[name].js',
publicPath: '/dist/' // Webpack dev middleware, if enabled, handles requests for this URL prefix
},

Sustitución del módulo de acceso rápido


Piense del Webpack activa sustitución del módulo característica (HMR ) como una evolución del Webpack Dev
Middleware. HMR incorpora las mismas ventajas, pero optimiza aún más el flujo de trabajo de desarrollo al
actualizar automáticamente el contenido de página después de compilar los cambios. No se debe confundir con
una actualización del explorador, lo que interferiría con el estado actual de en memoria y la sesión de depuración
de la aplicación SPA. Hay un vínculo directo entre el servicio de Middleware de desarrollo de Webpack y el
explorador, lo que significa que los cambios se insertan en el explorador.
Requisitos previos
Instale el software siguiente:
middleware caliente webpack npm paquete:

npm i -D webpack-hot-middleware

Configuración
El componente HMR debe estar registrado en la canalización de solicitudes HTTP de MVC en el Configure
método:

app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions {
HotModuleReplacement = true
});

Al igual que ocurría con Webpack Dev Middleware, UseWebpackDevMiddleware método de extensión se debe llamar
antes el UseStaticFiles método de extensión. Por motivos de seguridad, registre el middleware solo cuando la
aplicación se ejecuta en modo de desarrollo.
El webpack.config.js archivo debe definir una plugins de las matrices, incluso si se deja vacía:

module.exports = (env) => {


plugins: [new CheckerPlugin()]

Después de cargar la aplicación en el explorador, pestaña de la consola de las herramientas de desarrollo


proporciona confirmación de activación de HMR:

Aplicaciones auxiliares de enrutamientos


En la mayoría SPAs basada en núcleo de ASP.NET, es conveniente enrutamiento de cliente además del
enrutamiento del servidor. Los sistemas de enrutamiento SPA y MVC pueden trabajar independientemente sin
interferencias. Hay, sin embargo, un borde mayúsculas supone desafíos: identificación de respuestas HTTP 404.
Considere el escenario en el que una ruta sin extensión de /some/page se utiliza. Se supone la solicitud no hacer
coincidir el patrón una ruta de servidor, pero su patrón coincide con una ruta de cliente. Ahora considere una
solicitud entrante para /images/user-512.png , por lo general que espera encontrar un archivo de imagen en el
servidor. Si esa ruta de acceso del recurso solicitado no coincide con ninguna ruta de servidor o un archivo
estático, no es probable que la aplicación de cliente podría controlarla, por lo general desea devolver un código de
estado HTTP 404.
Requisitos previos
Instale el software siguiente:
El paquete de cliente de enrutamiento npm. Utilizando Angular como ejemplo:

npm i -S @angular/router

Configuración
Un método de extensión denominado MapSpaFallbackRoute se utiliza en el Configure método:

app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");

routes.MapSpaFallbackRoute(
name: "spa-fallback",
defaults: new { controller = "Home", action = "Index" });
});

Sugerencia: Las rutas se evalúan en el orden en el que está configurados. Por lo tanto, la default ruta en el
ejemplo de código anterior se utiliza en primer lugar para la coincidencia.

Crear un nuevo proyecto


JavaScriptServices proporciona plantillas de aplicaciones configuradas previamente. SpaServices se usa en estas
plantillas, junto con una diversidad de marcos y bibliotecas como Angular, React y Redux.
Estas plantillas se pueden instalar a través de la CLI de núcleo de .NET con el comando siguiente:

dotnet new --install Microsoft.AspNetCore.SpaTemplates::*

Se muestra una lista de plantillas SPA disponibles:

PLANTILLAS NOMBRE CORTO LENGUAJE ETIQUETAS

Núcleo de ASP.NET de MVC angular [C#] MVC/Web/SPA


con Angular

Núcleo de ASP.NET de MVC react [C#] MVC/Web/SPA


con React.js

Núcleo de ASP.NET MVC con reactredux [C#] MVC/Web/SPA


React.js y reducción

Para crear un nuevo proyecto con una de las plantillas SPA, incluya el nombre corto de la plantilla en el dotnet
nueva comando. El siguiente comando crea una aplicación Angular con ASP.NET MVC de núcleo configurado para
el lado del servidor:

dotnet new angular

Establecer el modo de configuración en tiempo de ejecución


Existen dos modos de configuración en tiempo de ejecución principal:
Desarrollo de:
Incluye asignaciones de origen para facilitar la depuración.
No optimice el código del lado cliente para el rendimiento.
Producción:
Excluye los mapas de código fuente.
Optimiza el código de cliente a través de agrupación y minificación.
ASP.NET Core utiliza una variable de entorno denominada ASPNETCORE_ENVIRONMENT para almacenar el modo de
configuración. Vea establecer el entorno de para obtener más información.
Ejecutar con .NET Core CLI
Restaurar el NuGet necesario y los paquetes de npm ejecutando el siguiente comando en la raíz del proyecto:

dotnet restore && npm i

Compilar y ejecutar la aplicación:

dotnet run

Inicia la aplicación en localhost según la el modo de configuración en tiempo de ejecución. Vaya a


http://localhost:5000 en el explorador muestra la página de aterrizaje.

Ejecutar con Visual Studio de 2017


Abra la .csproj archivo generado por la dotnet nueva comando. Los paquetes de NuGet y npm necesarios se
restauran automáticamente al proyecto abierto. Este proceso de restauración puede tardar unos minutos y está
lista para ejecutarse cuando finaliza la aplicación. Haga clic en el botón verde de ejecución o presione Ctrl + F5 , y
el explorador se abre en la página de inicio de la aplicación. La aplicación se ejecuta en localhost según la el modo
de configuración en tiempo de ejecución.

Probar la aplicación
Las plantillas de SpaServices están preconfiguradas para ejecutar pruebas del lado cliente mediante Karma y
Jasmine. Jasmine es un marco de pruebas unitarias popular para JavaScript, mientras que Karma es un ejecutor de
esas pruebas. Karma está configurado para funcionar con Webpack Dev Middleware, de forma que el
desarrollador no tenga que parar y ejecutar la prueba cada vez que se realicen cambios. Tanto si se ejecuta el
código en el caso de prueba como si se trata del propio caso de prueba, la prueba se ejecuta automáticamente.
Usar la aplicación Angular como ejemplo, dos casos de prueba criterio Comidos ya se proporcionan para que la
CounterComponent en el counter.component.spec.ts archivo:

it('should display a title', async(() => {


const titleText = fixture.nativeElement.querySelector('h1').textContent;
expect(titleText).toEqual('Counter');
}));

it('should start with count 0, then increments by 1 when clicked', async(() => {
const countElement = fixture.nativeElement.querySelector('strong');
expect(countElement.textContent).toEqual('0');

const incrementButton = fixture.nativeElement.querySelector('button');


incrementButton.click();
fixture.detectChanges();
expect(countElement.textContent).toEqual('1');
}));
Abra el símbolo del sistema en el ClientApp directory. Ejecute el siguiente comando:

npm test

La secuencia de comandos inicia el ejecutor de pruebas Karma, que lee la configuración definida en el karma.conf.js
archivo. Entre otras opciones, el karma.conf.js identifica los archivos de prueba para ejecutarse a través de su
files matriz:

module.exports = function (config) {


config.set({
files: [
'../../wwwroot/dist/vendor.js',
'./boot-tests.ts'
],

Publicar la aplicación
Combinación de los activos de cliente generados y los artefactos de ASP.NET Core publicados en un paquete listo
para implementar puede resultar complicada. Por suerte, SpaServices orquesta ese proceso de publicación
completa con un destino de MSBuild personalizado denominado RunWebpack :

<Target Name="RunWebpack" AfterTargets="ComputeFilesToPublish">


<!-- As part of publishing, ensure the JS resources are freshly built in production mode -->
<Exec Command="npm install" />
<Exec Command="node node_modules/webpack/bin/webpack.js --config webpack.config.vendor.js --env.prod" />
<Exec Command="node node_modules/webpack/bin/webpack.js --env.prod" />

<!-- Include the newly-built files in the publish output -->


<ItemGroup>
<DistFiles Include="wwwroot\dist\**; ClientApp\dist\**" />
<ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
<RelativePath>%(DistFiles.Identity)</RelativePath>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</ResolvedFileToPublish>
</ItemGroup>
</Target>

El destino de MSBuild tiene las siguientes responsabilidades:


1. Restaure los paquetes npm
2. Cree una versión de producción automatizado de los activos de terceros, de cliente
3. Cree una versión de producción automatizado de los activos de cliente personalizados
4. Copiar los activos Webpack generados en la carpeta de publicación
El destino de MSBuild se invoca cuando se ejecuta:

dotnet publish -c Release

Recursos adicionales
Documentos angulares
Uso de las plantillas de aplicación de una sola página
con ASP.NET Core
21/06/2018 • 2 minutes to read • Edit Online

NOTE
El SDK de .NET Core 2.0.x publicado incluye plantillas de proyecto anteriores para Angular, React y React con Redux. En esta
documentación no se tratan las plantillas de proyecto antiguas. En esta documentación se tratan las últimas plantillas para
Angular, React y React con Redux, que se pueden instalar manualmente en ASP.NET Core 2.0. Las plantillas se incluyen de
forma predeterminada con ASP.NET Core 2.1.

Requisitos previos
.NET Core SDK 2.0 or later
Node.js, versión 6 o posterior

Instalación
Las plantillas ya están instaladas con ASP.NET Core 2.1.
Si tiene ASP.NET Core 2.0, ejecute el comando siguiente para instalar las plantillas actualizadas de ASP.NET Core
para Angular, React y React con Redux:

dotnet new --install Microsoft.DotNet.Web.Spa.ProjectTemplates::2.0.0

Uso de las plantillas


Uso de la plantilla de proyecto Angular
Uso de la plantilla de proyecto React
Uso de la plantilla de proyecto React con Redux
Uso de la plantilla de proyecto de Angular con
ASP.NET Core
22/06/2018 • 21 minutes to read • Edit Online

NOTE
Esta documentación no trata sobre la plantilla de proyecto de Angular incluida en ASP.NET Core 2.0. Trata sobre la nueva
plantilla de Angular que puede actualizar manualmente. La plantilla se incluye de forma predeterminada en ASP.NET Core 2.1.

La plantilla de proyecto de Angular actualizada proporciona un práctico punto de partida para las aplicaciones
ASP.NET Core que usan Angular y la CLI de Angular para implementar una completa interfaz de usuario (UI) del
lado cliente.
La plantilla es equivalente a crear un proyecto de ASP.NET Core que funciona como back-end de API y un
proyecto de la CLI de Angular que funciona como interfaz de usuario. La plantilla ofrece la ventaja de hospedar
ambos tipos de proyecto en un único proyecto de aplicación. Por lo tanto, el proyecto de aplicación se puede
compilar y publicar como una sola unidad.

Creación de una nueva aplicación


Si usa ASP.NET Core 2.0, asegúrese de que ha instalado la plantilla de proyecto actualizada de React.
Si tiene ASP.NET Core 2.1 instalado, no hay ninguna necesidad de instalar la plantilla de proyecto Angular.
En un símbolo del sistema, cree un nuevo proyecto con el comando dotnet new angular en un directorio vacío.
Por ejemplo, los siguientes comandos crean la aplicación en un directorio my-new -app y cambian a ese directorio:

dotnet new angular -o my-new-app


cd my-new-app

Ejecute la aplicación desde Visual Studio o la CLI de .NET Core:


Visual Studio
CLI de .NET Core
Abra el archivo .csproj generado y, desde ahí, ejecute la aplicación de la manera habitual.
El proceso de compilación restaura las dependencias npm en la primera ejecución, lo que puede tardar varios
minutos. Las compilaciones posteriores son mucho más rápidas.
La plantilla de proyecto crea una aplicación ASP.NET Core y una aplicación de Angular. El uso previsto de la
aplicación ASP.NET Core es el acceso a los datos, la autorización y otros problemas relativos al servidor. Por otro
lado, la aplicación de Angular, que reside en el subdirectorio ClientApp está destinada a todos los problemas
relacionados con la interfaz de usuario.

Adición de páginas, imágenes, estilos, módulos, etc.


El directorio ClientApp contiene una aplicación estándar de la CLI de Angular. Consulte la documentación de
Angular para más información.
Existen pequeñas diferencias entre la aplicación de Angular creada con esta plantilla y la creada con la propia CLI
de Angular (mediante ng new ); sin embargo, las funcionalidades de la aplicación permanecen sin cambios. La
aplicación creada con la plantilla contiene un diseño basado en arranque y un ejemplo de enrutamiento básico.

Ejecución de comandos ng
En un símbolo del sistema, cambie al subdirectorio ClientApp:

cd ClientApp

Si tiene instalada la herramienta ng globalmente, puede ejecutar cualquiera de sus comandos. Por ejemplo,
puede ejecutar ng lint , ng test , o cualquiera de los otros comandos de la CLI de Angular. Si bien, no es
necesario ejecutar ng serve , dado que la aplicación ASP.NET Core se ocupa de atender las partes del lado cliente
y servidor de la aplicación. Internamente, usa ng serve en el desarrollo.
Si no tiene instalada la herramienta ng , ejecute en su lugar npm run ng . Por ejemplo, puede ejecutar
npm run ng lint o npm run ng test .

Instalar paquetes de npm


Para instalar paquetes de npm de otro fabricante, use un símbolo del sistema en el subdirectorio ClientApp. Por
ejemplo:

cd ClientApp
npm install --save <package_name>

Publicación e implementación
En el desarrollo, la aplicación se ejecuta en modo optimizado para comodidad del desarrollador. Por ejemplo, las
agrupaciones de JavaScript incluyen asignaciones de origen (de modo que, durante la depuración, puede ver el
código original de TypeScript). La aplicación inspecciona los cambios en los archivos de TypeScript, HTML y CSS
en el disco y, automáticamente, realiza una nueva compilación y recarga cuando observa que esos archivos han
cambiado.
En producción, use una versión de la aplicación que esté optimizada para el rendimiento. Esto se configura para
que tenga lugar automáticamente. Al publicar, la configuración de compilación emite una compilación Ahead Of
Time (AoT) reducida del código del lado cliente. A diferencia de la compilación de desarrollo, la compilación de
producción no requiere la instalación de Node.js en el servidor (a no ser que haya habilitado la representación
previa del lado servidor).
Puede usar métodos de implementación y hospedaje de ASP.NET Core estándar.

Ejecución de "ng serve" de manera independiente


El proyecto está configurado para iniciar su propia instancia del servidor de CLI de Angular en segundo plano
cuando la aplicación ASP.NET Core se inicia en modo de desarrollo. Esto resulta útil porque no tiene que ejecutar
manualmente un servidor independiente.
Sin embargo, esta configuración predeterminada tiene un inconveniente. Cada vez que modifica el código de C# y
la aplicación ASP.NET Core debe reiniciarse, el servidor de CLI de Angular se reinicia. Se necesitan unos 10
segundos para iniciar la copia de seguridad. Sin realiza frecuentes modificaciones en el código de C# y no quiere
esperar a que se reinicie la CLI de Angular, ejecute el servidor de la CLI de Angular externamente, con
independencia del proceso de ASP.NET Core. Para ello:
1. En un símbolo del sistema, cambie al subdirectorio ClientApp e inicie el servidor de desarrollo de la CLI
Angular:

cd ClientApp
npm start

IMPORTANT
Use npm start para iniciar el servidor de desarrollo de la CLI de Angular, no ng serve , de modo que la
configuración de package.json se respete. Para pasar parámetros adicionales al servidor de la CLI de Angular,
agréguelos a la línea de scripts correspondiente de su archivo package.json.

2. Modifique la aplicación ASP.NET Core para usar la instancia externa de la CLI de Angular en lugar de
iniciar una de las suyas. En la clase Startup, reemplace la invocación de spa.UseAngularCliServer por lo
siguiente:

spa.UseProxyToSpaDevelopmentServer("http://localhost:4200");

Cuando inicie la aplicación ASP.NET Core, no se iniciará un servidor de la CLI de Angular. En su lugar, se usa la
instancia que inició manualmente. Esto le permite iniciar y reiniciar con mayor rapidez. Ya no tiene que esperar a
que la CLI de Angular vuelva a compilar la aplicación cliente una y otra vez.

Representación del lado servidor


Como característica de rendimiento, puede elegir representar previamente su aplicación de Angular en el servidor,
así como ejecutarla en el cliente. Esto significa que los exploradores reciben marcado HTML que representa la
interfaz de usuario inicial de la aplicación, de modo que lo muestran incluso antes de descargar y ejecutar las
agrupaciones de JavaScript. La mayor parte de la implementación de esta representación procede de una
característica de Angular llamada Angular Universal.

TIP
Habilitar la representación del lado servidor (SSR) conlleva una serie de complicaciones adicionales tanto durante el
desarrollo como durante la implementación. La las desventajas de SSR para determinar si SSR es una buena opción para sus
requisitos.

Para habilitar SSR, deberá realizar varias adiciones al proyecto.


En la clase Startup, después de la línea que configura spa.Options.SourcePath , y antes de la llamada a
UseAngularCliServer o UseProxyToSpaDevelopmentServer , agregue el siguiente código:
app.UseSpa(spa =>
{
spa.Options.SourcePath = "ClientApp";

spa.UseSpaPrerendering(options =>
{
options.BootModulePath = $"{spa.Options.SourcePath}/dist-server/main.bundle.js";
options.BootModuleBuilder = env.IsDevelopment()
? new AngularCliBuilder(npmScript: "build:ssr")
: null;
options.ExcludeUrls = new[] { "/sockjs-node" };
});

if (env.IsDevelopment())
{
spa.UseAngularCliServer(npmScript: "start");
}
});

En modo de desarrollo, este código intenta compilar la agrupación de SSR mediante la ejecución del script
build:ssr , que se define en ClientApp\package.json. Esta acción compila una aplicación de Angular denominada
ssr , que aún no se ha definido.

Al final de la matriz apps en ClientApp/.angular-cli.json, defina una aplicación adicional con el nombre ssr . Use
las siguientes opciones:

{
"name": "ssr",
"root": "src",
"outDir": "dist-server",
"assets": [
"assets"
],
"main": "main.server.ts",
"tsconfig": "tsconfig.server.json",
"prefix": "app",
"scripts": [],
"environmentSource": "environments/environment.ts",
"environments": {
"dev": "environments/environment.ts",
"prod": "environments/environment.prod.ts"
},
"platform": "server"
}

Esta nueva configuración de aplicación habilitada para SSR requiere dos archivos más: tsconfig.server.json y
main.server.ts. El archivo tsconfig.server.json especifica las opciones de compilación de TypeScript. El archivo
main.server.ts sirve de punto de entrada de código durante SSR.
Agregue un nuevo archivo llamado tsconfig.server.json dentro de ClientApp/src (al lado del archivo
tsconfig.app.json existente), que contenga lo siguiente:
{
"extends": "../tsconfig.json",
"compilerOptions": {
"baseUrl": "./",
"module": "commonjs"
},
"angularCompilerOptions": {
"entryModule": "app/app.server.module#AppServerModule"
}
}

Este archivo configura el compilador AoT de Angular para buscar un módulo llamado app.server.module . Para
agregar el archivo, cree un nuevo archivo en ClientApp/src/app/app.server.module.ts (al lado del archivo
app.module.ts existente), que contenga lo siguiente:

import { NgModule } from '@angular/core';


import { ServerModule } from '@angular/platform-server';
import { ModuleMapLoaderModule } from '@nguniversal/module-map-ngfactory-loader';
import { AppComponent } from './app.component';
import { AppModule } from './app.module';

@NgModule({
imports: [AppModule, ServerModule, ModuleMapLoaderModule],
bootstrap: [AppComponent]
})
export class AppServerModule { }

Este módulo hereda del objeto app.module del lado cliente y define qué módulos adicionales de Angular están
disponibles durante SSR.
Recuerde que la nueva entrada ssr en .angular-cli.json hacía referencia a un archivo de punto de entrada llamado
main.server.ts. Ese archivo no se ha agregado aún, y ahora es el momento de hacerlo. Cree un nuevo archivo en
ClientApp/src/main.server.ts ( junto al archivo main.ts existente), que contenga lo siguiente:
import 'zone.js/dist/zone-node';
import 'reflect-metadata';
import { renderModule, renderModuleFactory } from '@angular/platform-server';
import { APP_BASE_HREF } from '@angular/common';
import { enableProdMode } from '@angular/core';
import { provideModuleMap } from '@nguniversal/module-map-ngfactory-loader';
import { createServerRenderer } from 'aspnet-prerendering';
export { AppServerModule } from './app/app.server.module';

enableProdMode();

export default createServerRenderer(params => {


const { AppServerModule, AppServerModuleNgFactory, LAZY_MODULE_MAP } = (module as any).exports;

const options = {
document: params.data.originalHtml,
url: params.url,
extraProviders: [
provideModuleMap(LAZY_MODULE_MAP),
{ provide: APP_BASE_HREF, useValue: params.baseUrl },
{ provide: 'BASE_URL', useValue: params.origin + params.baseUrl }
]
};

const renderPromise = AppServerModuleNgFactory


? /* AoT */ renderModuleFactory(AppServerModuleNgFactory, options)
: /* dev */ renderModule(AppServerModule, options);

return renderPromise.then(html => ({ html }));


});

El código de este archivo es lo que ejecuta ASP.NET Core en cada solicitud cuando ejecuta el middleware
UseSpaPrerendering que agregó a la clase Startup. Se encarga de recibir params del código de .NET (como la
dirección URL que se solicita), y realiza llamadas a las API de SSR de Angular para obtener el HTML resultante.
Estrictamente hablando, esto es suficiente para habilitar SSR en modo de desarrollo. Para que la aplicación
funcione correctamente cuando se publica, es fundamental realizar un cambio final. En el archivo .csproj principal
de la aplicación, establezca el valor de propiedad BuildServerSideRenderer en true :

<!-- Set this to true if you enable server-side prerendering -->


<BuildServerSideRenderer>true</BuildServerSideRenderer>

Este valor configura el proceso de compilación para ejecutar build:ssr durante la publicación e implementar los
archivos SSR en el servidor. Si no se habilita, SSR produce un error en producción.
Cuando la aplicación se ejecuta en modo de desarrollo o de producción, el código de Angular se representa
previamente como HTML en el servidor. El código del lado cliente se ejecuta con normalidad.
Paso de datos del código de .NET al código de TypeScript
Durante SSR, quizás quiera puede pasar datos por solicitud de la aplicación ASP.NET Core a la aplicación de
Angular. Por ejemplo, podría pasar información de cookies o algo que se haya leído de una base de datos. Para
ello, edite la clase Startup. En la devolución de llamada de UseSpaPrerendering , establezca un valor para
options.SupplyData , como el siguiente:

options.SupplyData = (context, data) =>


{
// Creates a new value called isHttpsRequest that's passed to TypeScript code
data["isHttpsRequest"] = context.Request.IsHttps;
};
La devolución de llamada SupplyData permite pasar datos arbitrarios y por solicitud que se pueden serializar con
JSON (por ejemplo, cadenas, valores booleanos o números). El código main.server.ts la recibe como params.data .
Por ejemplo, el ejemplo de código anterior pasa un valor booleano como params.data.isHttpsRequest a la
devolución de llamada createServerRenderer . Se puede pasar a otras partes de la aplicación en cualquier modo
admitido por Angular. Por ejemplo, vea cómo main.server.ts pasa el valor BASE_URL a cualquier componente cuyo
constructor se declara para recibirlo.
Desventajas de SSR
No todas las aplicaciones se benefician de SSR. La principal ventaja es el rendimiento percibido. Los visitantes que
llegan a la aplicación a través de una conexión de red lenta o en dispositivos móviles lentos verán la interfaz de
usuario inicial rápidamente, incluso si tarda un rato en capturar o analizar las agrupaciones de JavaScript. Sin
embargo, muchas SPA se utilizan principalmente a través de redes de empresa internas rápidas en equipos
rápidos donde la aplicación aparece casi al instante.
Al mismo tiempo, la habilitación de SSR conlleva importantes desventajas: Agrega complejidad al proceso de
desarrollo. El código se debe ejecutar en dos entornos diferentes: cliente y servidor (en un entorno de Node.js se
invoca desde ASP.NET Core). Hay algunos aspectos a tener en cuenta:
SSR requiere la instalación de Node.js en los servidores de producción. En algunos escenarios de
implementación esto se produce automáticamente, como Azure App Services, pero no en otros, como
Azure Service Fabric.
Al habilitar la marca de compilación BuildServerSideRenderer , el directorio node_modules se publica. Esta
carpeta contiene más de 20 000 archivos, lo que aumenta el tiempo de implementación.
Para ejecutar el código en un entorno de Node.js, no puede confiar en la existencia de API de JavaScript
específicas del explorador como window o localStorage . Si el código (o alguna biblioteca de terceros a la
que hace referencia) intenta usar estas API, obtendrá un error durante SSR. Por ejemplo, no use jQuery
porque hace referencia a API específicas del explorador en muchos lugares. Para evitar errores, no use en la
medida de lo posible SSR ni API o bibliotecas específicas del explorador. Puede encapsular las llamadas a
estas API en comprobaciones para asegurarse de que no se invoquen durante SSR. Por ejemplo, use una
comprobación como la siguiente en código de JavaScript o TypeScript:

if (typeof window !== 'undefined') {


// Call browser-specific APIs here
}
Uso de la plantilla de proyecto de React con ASP.NET
Core
22/06/2018 • 8 minutes to read • Edit Online

NOTE
Esta documentación no trata sobre la plantilla de proyecto de React incluida en ASP.NET Core 2.0. Trata sobre la nueva
plantilla de React que puede actualizar manualmente. La plantilla se incluye de forma predeterminada en ASP.NET Core 2.1.

La plantilla de proyecto actualizada de React ofrece un práctico punto de partida para las aplicaciones ASP.NET
Core que usan React y las convenciones create-react-app (CRA) para implementar una completa interfaz de
usuario (UI) en el lado cliente.
La plantilla es equivalente a crear un proyecto de ASP.NET Core para que funcione como un back-end de API y un
proyecto de React de CRA estándar para que funcione como interfaz de usuario, pero con la comodidad de
hospedar ambos en un único proyecto de aplicación que se puede compilar y publicar como una sola unidad.

Creación de una nueva aplicación


Si usa ASP.NET Core 2.0, asegúrese de que ha instalado la plantilla de proyecto actualizada de React.
Si tiene ASP.NET Core 2.1 instalado, no hay ninguna necesidad de instalar la plantilla de proyecto de reaccionar.
En un símbolo del sistema, cree un nuevo proyecto con el comando dotnet new react en un directorio vacío. Por
ejemplo, los siguientes comandos crean la aplicación en un directorio my-new -app y cambian a ese directorio:

dotnet new react -o my-new-app


cd my-new-app

Ejecute la aplicación desde Visual Studio o la CLI de .NET Core:


Visual Studio
CLI de .NET Core
Abra el archivo .csproj generado y, desde ahí, ejecute la aplicación de la manera habitual.
El proceso de compilación restaura las dependencias npm en la primera ejecución, lo que puede tardar varios
minutos. Las compilaciones posteriores son mucho más rápidas.
La plantilla de proyecto crea una aplicación ASP.NET Core y una aplicación de React. El uso previsto de la
aplicación ASP.NET Core es el acceso a los datos, la autorización y otros problemas relativos al servidor. La
aplicación de React, que reside en el subdirectorio ClientApp, está diseñada para utilizarse para su uso con todos
los problemas de la interfaz de usuario.

Adición de páginas, imágenes, estilos, módulos, etc.


El directorio ClientApp es una aplicación de React de CRA estándar. Para más información, consulte la
documentación oficial de CRA.
Existe pequeñas diferencias entre la aplicación de React creada mediante este plantilla y la creada mediante CRA
propiamente dicho; sin embargo, las funcionalidades de la aplicación permanecen sin cambios. La aplicación
creada con la plantilla contiene un diseño basado en arranque y un ejemplo de enrutamiento básico.

Instalar paquetes de npm


Para instalar paquetes de npm de otro fabricante, use un símbolo del sistema en el subdirectorio ClientApp. Por
ejemplo:

cd ClientApp
npm install --save <package_name>

Publicación e implementación
En el desarrollo, la aplicación se ejecuta en modo optimizado para comodidad del desarrollador. Por ejemplo,
agrupaciones de JavaScript contienen asignaciones de origen (de modo que durante la depuración, puede ver el
código fuente original). La aplicación inspecciona los cambios en los archivos de JavaScript, HTML y CSS en el
disco y, automáticamente, realiza una nueva compilación y recarga cuando observa que esos archivos han
cambiado.
En producción, use una versión de la aplicación que esté optimizada para el rendimiento. Esto se configura para
que tenga lugar automáticamente. Al publicar, la configuración de compilación emite una compilación transpilada
reducida del código de cliente. A diferencia de la compilación de desarrollo, la compilación de producción no
requiere la instalación de Node.js en el servidor.
Puede usar métodos de implementación y hospedaje de ASP.NET Core estándar.

Ejecución del servidor de CRA de forma independiente


El proyecto está configurado para iniciar su propia instancia del servidor de desarrollo de CRA en segundo plano
cuando la aplicación ASP.NET Core se inicia en modo de desarrollo. Esto resulta útil porque significa que no tiene
que ejecutar manualmente un servidor independiente.
Sin embargo, esta configuración predeterminada tiene un inconveniente. Cada vez que modifica el código de C# y
la aplicación ASP.NET Core debe reiniciarse, el servidor de CRA se reinicia. Se necesitan unos segundos para
iniciar la copia de seguridad. Sin realiza frecuentes modificaciones en el código de C# y no quiere esperar a que se
reinicie el servidor de CRA, ejecute el servidor de CRA externamente, con independencia del proceso de ASP.NET
Core. Para ello:
1. En un símbolo del sistema, cambie al subdirectorio ClientApp e inicie el servidor de desarrollo de CRA:

cd ClientApp
npm start

2. Modifique la aplicación ASP.NET Core para usar la instancia del servidor de CRA externo en lugar de
iniciar una de las suyas. En la clase Startup, reemplace la invocación de spa.UseReactDevelopmentServer por
lo siguiente:

spa.UseProxyToSpaDevelopmentServer("http://localhost:3000");

Cuando inicie la aplicación ASP.NET Core, no se iniciará un servidor de CRA. En su lugar, se usa la instancia que
inició manualmente. Esto le permite iniciar y reiniciar con mayor rapidez. Ya no tiene que esperar a que la
aplicación de React se recompile de una vez a otra.
Uso de la plantilla de proyecto React-with-Redux con
ASP.NET Core
22/06/2018 • 2 minutes to read • Edit Online

NOTE
Esta documentación no trata sobre la plantilla de proyecto de React-with-Redux incluida en ASP.NET Core 2.0. Trata sobre la
nueva plantilla de React-with-Redux que puede actualizar manualmente. La plantilla se incluye de forma predeterminada en
ASP.NET Core 2.1.

La plantilla de proyecto actualizada de React-with-Redux ofrece un práctico punto de partida para las aplicaciones
ASP.NET Core que usan React, Redux y las convenciones create-react-app (CRA) para implementar una completa
interfaz de usuario (UI) en el lado cliente.
A excepción del comando de creación del proyecto, toda la información sobre la plantilla de React-with-Redux es
igual que la de la plantilla de React. Para crear este tipo de proyecto, ejecute dotnet new reactredux en lugar de
dotnet new react . Para más información sobre la funcionalidad común a ambas plantillas basadas en React,
consulte la documentación de la plantilla de React.
SignalR de ASP.NET Core
25/06/2018 • 2 minutes to read • Edit Online

Introducción
Introducción
Concentradores
Cliente de JavaScript
Cliente .NET
HubContext
Usuarios y grupos
Protocolo de concentrador MessagePack
Publicar en Azure
Streaming
Plataformas compatibles
Introducción a ASP.NET Core SignalR
22/06/2018 • 4 minutes to read • Edit Online

Por Rachel Appel

¿Qué es SignalR?
Núcleo de ASP.NET SignalR es una biblioteca que simplifica la agregación de funcionalidades de web en tiempo
real a las aplicaciones. La funcionalidad web en tiempo real permite que el código de servidor para el contenido de
inserción a los clientes al instante.
Buenos candidatos para SignalR:
Aplicaciones que requieren alta frecuencia actualizaciones desde el servidor. Se trata de juegos, redes sociales,
votar, subasta, mapas y aplicaciones GPS.
Los paneles y supervisión de aplicaciones. Los ejemplos incluyen paneles de la empresa, las actualizaciones de
ventas instantáneas, o alertas de viaje.
Aplicaciones de colaboración. Aplicaciones de pizarra y reunión software de equipo son ejemplos de
aplicaciones de colaboración.
Aplicaciones que requieren las notificaciones. Las redes sociales, correo electrónico, chat, juegos, alertas de
viaje y muchas otras aplicaciones utilizan notificaciones.
SignalR proporciona una API para la creación de servidor a cliente llamadas a procedimiento remoto (RPC ). Las
RPC llamar a funciones de JavaScript en los clientes desde el código de .NET Core en el servidor de.
SignalR principales de ASP.NET:
Controla automáticamente la administración de conexiones.
Permite difundir mensajes a todos los clientes conectados al mismo tiempo. Por ejemplo, un salón de chat.
Habilita el envío de mensajes a clientes específicos o grupos de clientes.
Es código abierto en GitHub.
Escalable.
La conexión entre el cliente y el servidor es persistente, a diferencia de una conexión HTTP.

Transportes
Resúmenes de SignalR a través de una serie de técnicas para crear aplicaciones web en tiempo real. WebSockets
es el transporte óptimo, pero se pueden usar otras técnicas como eventos de Server-Sent y sondeo largo cuando
los que no están disponibles. SignalR detectará automáticamente e inicializar el transporte adecuado en función de
las características admitidas en el servidor y el cliente.

Concentradores
SignalR usa centros para la comunicación entre clientes y servidores.
Un concentrador es una canalización de alto nivel que permite que el cliente y servidor para llamar a métodos en
entre sí. SignalR administra el envío a través de los límites del equipo automáticamente, permitiendo a los clientes
llamar a métodos en el servidor como fácilmente como métodos locales y viceversa. Los concentradores permiten
pasar parámetros fuertemente tipados a métodos, lo que permite el enlace de modelos. SignalR proporciona dos
protocolos de concentrador integrada: un protocolo de texto en función de JSON y un protocolo binario basado en
MessagePack. Por lo general, MessagePack crea mensajes más pequeños que cuando se usan JSON. Deben ser
compatible con los exploradores más antiguos nivel XHR 2 para proporcionar compatibilidad con el protocolo
MessagePack.
Los centros de llamar a código de cliente mediante el envío de mensajes mediante el transporte activo. Los
mensajes contienen el nombre y los parámetros del método de cliente. Objetos que se envíen como parámetros de
método se deserializan mediante el protocolo configurado. El cliente intenta hacer coincidir el nombre a un
método en el código de cliente. Cuando se produce una coincidencia, el método de cliente se ejecuta con los datos
del parámetro deserializado.

Recursos adicionales
Empezar a trabajar con SignalR para ASP.NET Core
Plataformas compatibles
Concentradores
Cliente de JavaScript
Use los concentradores de SignalR para ASP.NET
Core
22/06/2018 • 6 minutes to read • Edit Online

Por Rachel Appel y Kevin Griffin


Ver o descargar el código de ejemplo (cómo descargar)

¿Qué es un concentrador SignalR


La API de concentradores de SignalR permite llamar a métodos en los clientes conectados desde el servidor. En el
código del servidor, se definen métodos llamados por el cliente. En el código de cliente, se definen métodos que se
llaman desde el servidor. SignalR se encarga de todo el contenido en segundo plano que permite las
comunicaciones de cliente a servidor y cliente de servidor en tiempo real.

Configurar los concentradores SignalR


El middleware de SignalR requiere algunos servicios, que se configuran mediante una llamada a
services.AddSignalR .

services.AddSignalR();

Al agregar la funcionalidad de SignalR a una aplicación de ASP.NET Core, el programa de instalación SignalR
rutas mediante una llamada a app.UseSignalR en el Startup.Configure método.

app.UseSignalR(route =>
{
route.MapHub<ChatHub>("/chathub");
});

Crear y usar los centros


Crear un concentrador mediante la declaración de una clase que hereda de Hub y agregar métodos públicos. Los
clientes pueden llamar a métodos que se definen como public .
public class ChatHub : Hub
{
public async Task SendMessage(string user, string message)
{
await Clients.All.SendAsync("ReceiveMessage", user,message);
}

public Task SendMessageToCaller(string message)


{
return Clients.Caller.SendAsync("ReceiveMessage", message);
}

public Task SendMessageToGroups(string message)


{
List<string> groups = new List<string>() { "SignalR Users" };
return Clients.Groups(groups).SendAsync("ReceiveMessage", message);
}

public override async Task OnConnectedAsync()


{
await Groups.AddToGroupAsync(Context.ConnectionId, "SignalR Users");
await base.OnConnectedAsync();
}

public override async Task OnDisconnectedAsync(Exception exception)


{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, "SignalR Users");
await base.OnDisconnectedAsync(exception);
}
}

Puede especificar un tipo de valor devuelto y parámetros, incluidos los tipos complejos y matrices, como lo haría
en cualquier método de C#. SignalR controla la serialización y deserialización de objetos complejos y matrices en
los parámetros y valores devueltos.

El objeto de los clientes


Cada instancia de la Hub clase tiene una propiedad denominada Clients que contiene los miembros siguientes
para la comunicación entre cliente y servidor:

PROPERTY DESCRIPCIÓN

All Llama a un método en todos los clientes conectados

Caller Llama a un método en el cliente que invoca el método de


concentrador

Others Llama a un método en todos los clientes conectados, excepto


el cliente que invocó el método

Además, Hub.Clients contiene los siguientes métodos:

MÉTODO DESCRIPCIÓN

AllExcept Llama a un método en todos los clientes conectados, excepto


para las conexiones especificadas

Client Llama a un método en un cliente conectado específico


MÉTODO DESCRIPCIÓN

Clients Llama a un método en los clientes conectados específicos

Group Llama a un método a todas las conexiones en el grupo


especificado

GroupExcept Llama a un método a todas las conexiones en el grupo


especificado, excepto las conexiones especificadas

Groups Llama a un método a varios grupos de conexiones

OthersInGroup Llama a un método a un grupo de conexiones, excepto al


cliente que invoca el método de concentrador

User Llama a un método para todas las conexiones asociadas a un


usuario específico

Users Llama a un método para todas las conexiones asociadas a los


usuarios especificados

Cada propiedad o método de las tablas anteriores devuelve un objeto con un SendAsync método. El SendAsync
método le permite especificar el nombre y los parámetros del método de cliente para llamar a.

Enviar mensajes a los clientes


Para realizar llamadas a clientes específicos, use las propiedades de la Clients objeto. En el ejemplo siguiente, la
SendMessageToCaller método muestra cómo enviar un mensaje a la conexión que se invoca el método de
concentrador. El SendMessageToGroups método envía un mensaje a los grupos almacenados en un List
denominado groups .

public Task SendMessageToCaller(string message)


{
return Clients.Caller.SendAsync("ReceiveMessage", message);
}

public Task SendMessageToGroups(string message)


{
List<string> groups = new List<string>() { "SignalR Users" };
return Clients.Groups(groups).SendAsync("ReceiveMessage", message);
}

Controlar eventos de una conexión


La API de concentradores de SignalR proporciona el OnConnectedAsync y OnDisconnectedAsync métodos virtuales
para administrar y realizar un seguimiento de las conexiones. Invalidar el OnConnectedAsync método virtual para
realizar acciones cuando un cliente se conecta al concentrador, por ejemplo, éste se agrega a un grupo.
public override async Task OnConnectedAsync()
{
await Groups.AddToGroupAsync(Context.ConnectionId, "SignalR Users");
await base.OnConnectedAsync();
}

public override async Task OnDisconnectedAsync(Exception exception)


{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, "SignalR Users");
await base.OnDisconnectedAsync(exception);
}

Controlar errores
Las excepciones producidas en los métodos de concentrador se envían al cliente que invocó el método. En el
cliente de JavaScript, el invoke método devuelve un compromiso de JavaScript. Cuando el cliente recibe un error
con un controlador asociado a la promesa mediante catch , se invoca y se pasan como JavaScript Error objeto.

connection.invoke("SendMessage", user, message).catch(err => console.error(err));

Recursos relacionados
Introducción a ASP.NET Core SignalR
Cliente de JavaScript
Publicar en Azure
Cliente de SignalR JavaScript Core ASP.NET
22/06/2018 • 6 minutes to read • Edit Online

Por Rachel Appel


La biblioteca de cliente de ASP.NET Core SignalR JavaScript permite a los desarrolladores llamar a código de
concentrador de servidor.
Vea o descargue el código de ejemplo (cómo descargarlo)

Instale el paquete de cliente de SignalR


La biblioteca de cliente de SignalR JavaScript se entrega como un npm paquete. Si está utilizando Visual Studio,
ejecute npm install desde el Package Manager Console mientras se encuentre en la carpeta raíz. Para el
código de Visual Studio, ejecute el comando desde el Terminal integrado.

npm init -y
npm install @aspnet/signalr

NPM instala el contenido del paquete en el *node_modules\ @aspnet\signalr\dist\browser* carpeta. Cree una
carpeta nueva denominada signalr en el wwwroot\lib carpeta. Copia la signalr.js del archivo a la
wwwroot\lib\signalr carpeta.

Usar al cliente de SignalR JavaScript


Referencia de cliente de SignalR JavaScript de la <script> elemento.

<script src="~/lib/signalr/signalr.js"></script>

Conectarse a un concentrador
El código siguiente se crea e inicia una conexión. Nombre del concentrador distingue mayúsculas de minúsculas.

const connection = new signalR.HubConnectionBuilder()


.withUrl("/chatHub")
.configureLogging(signalR.LogLevel.Information)
.build();
connection.start().catch(err => console.error(err.toString()));

Conexiones entre orígenes


Por lo general, los exploradores cargar las conexiones desde el mismo dominio que la página solicitada. Sin
embargo, hay ocasiones cuando se requiere una conexión a otro dominio.
Para evitar que un sitio malintencionado pueda leer los datos confidenciales desde otro sitio, las conexiones entre
orígenes están deshabilitadas de forma predeterminada. Para permitir que una solicitud entre orígenes, habilitarla
en la Startup clase.
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using SignalRChat.Hubs;

namespace SignalRChat
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}

public IConfiguration Configuration { get; }

public void ConfigureServices(IServiceCollection services)


{
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});

services.AddMvc();

services.AddCors(options => options.AddPolicy("CorsPolicy",


builder =>
{
builder.AllowAnyMethod().AllowAnyHeader()
.WithOrigins("http://localhost:55830")
.AllowCredentials();
}));

services.AddSignalR();
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)


{
if (env.IsDevelopment())
{
app.UseBrowserLink();
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseCors("CorsPolicy");
app.UseSignalR(routes =>
{
routes.MapHub<ChatHub>("/chathub");
});
app.UseMvc();
}
}
}
Llamar a métodos de concentrador de cliente
Los clientes de JavaScript llama al métodos públicos en concentradores por usando connection.invoke . El
invoke método acepta dos argumentos:

El nombre del método de concentrador. En el ejemplo siguiente, el nombre de la base de datos central es
SendMessage .
Los argumentos definidos en el método de concentrador. En el ejemplo siguiente, el nombre del argumento es
message .

connection.invoke("SendMessage", user, message).catch(err => console.error(err.toString()));

Llamar a métodos de cliente desde el concentrador


Para recibir mensajes desde el concentrador, defina un método mediante el connection.on método.
El nombre del método de cliente de JavaScript. En el ejemplo siguiente, el nombre de método es
ReceiveMessage .
Argumentos de que la central se pasa al método. En el ejemplo siguiente, el valor del argumento es message .

connection.on("ReceiveMessage", (user, message) => {


const encodedMsg = user + " says " + message;
const li = document.createElement("li");
li.textContent = encodedMsg;
document.getElementById("messagesList").appendChild(li);
});

El código anterior en connection.on se ejecuta cuando llama a código del lado servidor mediante el SendAsync
método.

public async Task SendMessage(string user, string message)


{
await Clients.All.SendAsync("ReceiveMessage", user, message);
}

SignalR determina a qué método de cliente para llamar a haciendo coincidir el nombre de método y los
argumentos definan en SendAsync y connection.on .

NOTE
Como práctica recomendada, llame a connection.start después connection.on por lo que los controladores están
registrados antes de que se reciben los mensajes.

Registro y control de errores


Cadena de un catch método al final de la connection.start método para controlar los errores de cliente. Use
console.error para errores de salida a la consola del explorador.

connection.start().catch(err => console.error(err.toString()));

Configurar la traza de registro del lado cliente pasando un registrador y el tipo de evento que se registran cuando
se realiza la conexión. Los mensajes se registran con el nivel de registro especificado y versiones posteriores.
Niveles de registro disponibles son los siguientes:
signalR.LogLevel.Error : Mensajes de error. Registros Error sólo mensajes.
signalR.LogLevel.Warning : Mensajes de advertencia acerca de posibles errores. Registros de Warning , y
Error mensajes.
signalR.LogLevel.Information : Mensajes de estado sin errores. Registros de Information , Warning , y Error
mensajes.
signalR.LogLevel.Trace : Mensajes de seguimiento. Registra todo, incluidos los datos transportados entre
cliente y concentrador.
Use la configureLogging método en HubConnectionBuilder para configurar el nivel de registro. Los mensajes se
registran en la consola del explorador.

const connection = new signalR.HubConnectionBuilder()


.withUrl("/chatHub")
.configureLogging(signalR.LogLevel.Information)
.build();

Recursos relacionados
Concentradores
Cliente .NET
Publicar en Azure
Permitir solicitudes entre orígenes (CORS ) en ASP.NET Core
Cliente de .NET Core de ASP.NET SignalR
22/06/2018 • 3 minutes to read • Edit Online

Por Rachel Appel


El cliente de ASP.NET Core SignalR .NET puede utilizarse en aplicaciones de Xamarin, WPF, Windows Forms,
consola y .NET Core. Al igual que el cliente JavaScript, el cliente de .NET permite recibir y enviar y recibir
mensajes a un concentrador en tiempo real.
Vea o descargue el código de ejemplo (cómo descargarlo)
El ejemplo de código de este artículo es una aplicación WPF que utiliza al cliente de ASP.NET Core SignalR. NET.

Instale el paquete de cliente .NET de SignalR


El Microsoft.AspNetCore.SignalR.Client paquete es necesario para que los clientes de .NET para conectarse a los
concentradores SignalR. Para instalar la biblioteca de cliente, ejecute el siguiente comando el Package Manager
Console ventana:

Install-Package Microsoft.AspNetCore.SignalR.Client

Conectarse a un concentrador
Para establecer una conexión, cree un HubConnectionBuilder y llame a Build . La dirección URL del concentrador,
protocolo, tipo de transporte, nivel de registro, encabezados y otras opciones se pueden configurar durante la
creación de una conexión. Configurar las opciones necesarias mediante la inserción de cualquiera de los
HubConnectionBuilder métodos en Build . Iniciar la conexión con StartAsync .
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.Logging;
using System;
using System.Windows;

namespace SignalRChatClient
{
public partial class MainWindow : Window
{
HubConnection connection;
public MainWindow()
{
InitializeComponent();

connection = new HubConnectionBuilder()


.WithUrl("https://localhost:44317/ChatHub")
.Build();
}

private async void connectButton_Click(object sender, RoutedEventArgs e)


{
connection.On<string, string>("ReceiveMessage", (user, message) =>
{
this.Dispatcher.Invoke(() =>
{
var newMessage = $"{user}: {message}";
messagesList.Items.Add(newMessage);
});
});

try
{
await connection.StartAsync();
messagesList.Items.Add("Connection started");
connectButton.IsEnabled = false;
sendButton.IsEnabled = true;
}
catch (Exception ex)
{
messagesList.Items.Add(ex.Message);
}
}

private async void sendButton_Click(object sender, RoutedEventArgs e)


{
try
{
await connection.InvokeAsync("SendMessage",
userTextBox.Text, messageTextBox.Text);
}
catch (Exception ex)
{
messagesList.Items.Add(ex.Message);
}
}
}
}

Llamar a métodos de concentrador de cliente


InvokeAsync llama a métodos en el concentrador. Pasar el nombre del método de concentrador y los argumentos
definidos en el método de concentrador a InvokeAsync . SignalR es asincrónica, por lo que usar async y await
cuando se realizan las llamadas.
await connection.InvokeAsync("SendMessage",
userTextBox.Text, messageTextBox.Text);

Llamar a métodos de cliente desde el concentrador


Define el centro de llamadas mediante el método connection.On tras su creación, pero antes de iniciar la conexión.

connection.On<string, string>("ReceiveMessage", (user, message) =>


{
this.Dispatcher.Invoke(() =>
{
var newMessage = $"{user}: {message}";
messagesList.Items.Add(newMessage);
});
});

El código anterior en connection.On se ejecuta cuando llama a código del lado servidor mediante el SendAsync
método.

public async Task SendMessage(string user, string message)


{
await Clients.All.SendAsync("ReceiveMessage", user,message);
}

Registro y control de errores


Controlar los errores con una instrucción try-catch. Inspeccionar el Exception para determinar la acción
apropiada deben realizar después de que se produce un error.

try
{
await connection.InvokeAsync("SendMessage",
userTextBox.Text, messageTextBox.Text);
}
catch (Exception ex)
{
messagesList.Items.Add(ex.Message);
}

Recursos adicionales
Concentradores
Cliente de JavaScript
Publicar en Azure
Enviar mensajes desde fuera de un concentrador
22/06/2018 • 2 minutes to read • Edit Online

Por Mikael Mengistu


El concentrador de SignalR es la abstracción básica para enviar mensajes a los clientes conectados al servidor de
SignalR. También es posible enviar mensajes desde otros lugares en la aplicación mediante el IHubContext service.
Este artículo explica cómo obtener acceso a un SignalR IHubContext para enviar notificaciones a los clientes desde
fuera de un concentrador.
Ver o descargar el código de ejemplo (cómo descargar)

Obtener una instancia de IHubContext


En ASP.NET Core SignalR, puede tener acceso a una instancia de IHubContext a través de la inserción de
dependencias. También puede insertar una instancia de IHubContext en un controlador, middleware u otro servicio
de DI. Utilice la instancia para enviar mensajes a los clientes.

NOTE
Esto difiere de ASP.NET SignalR que utiliza GlobalHost para proporcionar acceso a la IHubContext . ASP.NET Core tiene un
marco de inyección de dependencia que elimina la necesidad de este singleton global.

Insertar una instancia de IHubContext en un controlador


También puede insertar una instancia de IHubContext en un controlador, éste se agrega a su constructor:

public class HomeController : Controller


{
private readonly IHubContext<NotificationHub> _hubContext;

public HomeController(IHubContext<NotificationHub> hubContext)


{
_hubContext = hubContext;
}
}

Ahora, con acceso a una instancia de IHubContext , puede llamar a métodos de concentrador como si estuviera en
el concentrador de sí mismo.

public async Task<IActionResult> Index()


{
await _hubContext.Clients.All.SendAsync("Notify", $"Home page loaded at: {DateTime.Now}");
return View();
}

Obtener una instancia de IHubContext de middleware


Acceso a la IHubContext dentro de la canalización de middleware de este modo:
app.Use(next => (context) =>
{
var hubContext = (IHubContext<MyHub>)context
.RequestServices
.GetServices<IHubContext<MyHub>>();
//...
});

NOTE
Cuando se llaman a métodos de concentrador desde fuera de la Hub clase, no hay ningún autor de llamada asociado a la
invocación. Por lo tanto, no hay ningún acceso a la ConnectionId , Caller , y Others propiedades.

Recursos relacionados
Introducción
Concentradores
Publicar en Azure
Administrar usuarios y grupos de SignalR
22/06/2018 • 3 minutes to read • Edit Online

Por Brennan Conroy


SignalR permite que los mensajes se envíen a todas las conexiones asociadas a un usuario específico, así como el
nombre de grupos de conexiones.
Ver o descargar el código de ejemplo (cómo descargar)

Usuarios de SignalR
SignalR le permite enviar mensajes a todas las conexiones asociadas a un usuario específico. De forma
predeterminada se usa SignalR el ClaimTypes.NameIdentifier desde el ClaimsPrincipal asociado a la conexión
como el identificador de usuario. Un único usuario puede tener varias conexiones a una aplicación de SignalR. Por
ejemplo, un usuario podría estar conectado en el escritorio, así como el número de teléfono. Cada dispositivo tiene
una conexión SignalR independiente, pero están asociadas con el mismo usuario. Si se envía un mensaje al
usuario, todas las conexiones asociadas a ese usuario recibe el mensaje.
Enviar un mensaje a un usuario específico, pase el identificador de usuario para el User funcionando en su
método de concentrador, tal como se muestra en el ejemplo siguiente:

NOTE
El identificador de usuario distingue mayúsculas de minúsculas.

public Task SendPrivateMessage(string user, string message)


{
return Clients.User(user).SendAsync("ReceiveMessage", message);
}

El identificador de usuario se puede personalizar mediante la creación de un IUserIdProvider y registrarlo en


ConfigureServices .

public class CustomUserIdProvider : IUserIdProvider


{
public virtual string GetUserId(HubConnectionContext connection)
{
return connection.User?.FindFirst(ClaimTypes.Email)?.Value;
}
}

public void ConfigureServices(IServiceCollection services)


{
services.AddSignalR();

services.AddSingleton<IUserIdProvider, CustomUserIdProvider>();
}
NOTE
AddSignalR debe llamarse antes de registrar los servicios SignalR personalizados.

Grupos de SignalR
Un grupo es una colección de conexiones asociadas a un nombre. Los mensajes pueden enviarse a todas las
conexiones en un grupo. Los grupos son el método recomendado para enviar a una conexión o varias conexiones
porque los grupos administrados por la aplicación. Una conexión puede ser un miembro de varios grupos. Esto
hace que los grupos ideal para algo parecido a una aplicación de chat, donde cada habitación puede representarse
como un grupo. Las conexiones se pueden agregar a o quitar de los grupos a través de la AddToGroupAsync y
RemoveFromGroupAsync métodos.

public async Task AddToGroup(string groupName)


{
await Groups.AddToGroupAsync(Context.ConnectionId, groupName);

await Clients.Group(groupName).SendAsync("Send", $"{Context.ConnectionId} has joined the group


{groupName}.");
}

public async Task RemoveFromGroup(string groupName)


{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName);

await Clients.Group(groupName).SendAsync("Send", $"{Context.ConnectionId} has left the group


{groupName}.");
}

Pertenencia a grupos no se conserva cuando se vuelve a conectar una conexión. Debe volver a unirse al grupo
cuando se vuelve a establecer la conexión. No es posible contar a los miembros de un grupo, puesto que esta
información no está disponible si la aplicación se escala a varios servidores.

NOTE
Nombres de grupo distinguen entre mayúsculas y minúsculas.

Recursos relacionados
Introducción
Concentradores
Publicar en Azure
Utilice el protocolo de concentrador de MessagePack
en SignalR para ASP.NET Core
22/06/2018 • 4 minutes to read • Edit Online

Por Brennan Conroy


En este artículo se da por supuesto que el lector está familiarizado con los temas tratados en Introducción.

¿Qué es MessagePack?
MessagePack es un formato de serialización binaria es rápido y compacto. Es útil cuando el rendimiento y el ancho
de banda son un problema porque crea mensajes más pequeños en comparación con JSON. Dado que es un
formato binario, mensajes no son legibles cuando se examinan los registros y seguimientos de red a menos que
los bytes se pasan a través de un analizador de MessagePack. SignalR tiene compatibilidad integrada para el
formato MessagePack y proporciona las API para el cliente y el servidor usar.

Configurar MessagePack en el servidor


Para habilitar el protocolo de concentrador MessagePack en el servidor, instale el
Microsoft.AspNetCore.SignalR.Protocols.MessagePack paquete de la aplicación. En el archivo Startup.cs agregue
AddMessagePackProtocol a la AddSignalR llamada para habilitar la compatibilidad de MessagePack en el servidor.

NOTE
JSON está habilitada de forma predeterminada. Agregar MessagePack habilita la compatibilidad para los clientes de JSON y
MessagePack.

services.AddSignalR()
.AddMessagePackProtocol();

Para personalizar cómo MessagePack dará formato a los datos, AddMessagePackProtocol toma un delegado para
configurar las opciones. En ese delegado, el FormatterResolvers propiedad puede utilizarse para configurar las
opciones de serialización MessagePack. Para obtener más información sobre cómo funcionan los solucionadores,
visite la biblioteca de MessagePack en MessagePack CSharp. Los atributos se pueden utilizar en los objetos que
desea serializar para definir cómo debe controlarse.

services.AddSignalR()
.AddMessagePackProtocol(options =>
{
options.FormatterResolvers = new List<MessagePack.IFormatterResolver>()
{
MessagePack.Resolvers.StandardResolver.Instance
};
});

Configurar MessagePack en el cliente


Cliente .NET
Para habilitar MessagePack en el cliente. NET, instale el Microsoft.AspNetCore.SignalR.Protocols.MessagePack
paquete y llame al método AddMessagePackProtocol en HubConnectionBuilder .

var hubConnection = new HubConnectionBuilder()


.WithUrl("/chatHub")
.AddMessagePackProtocol()
.Build();

NOTE
Esto AddMessagePackProtocol llamada toma un delegado para configurar opciones como el servidor.

Cliente de JavaScript
MessagePack para el cliente de Javascript se admiten por los @aspnet/signalr-protocol-msgpack paquete NPM.

npm install @aspnet/signalr-protocol-msgpack

Después de instalar el paquete npm, el módulo se puede usar directamente mediante un cargador de módulos de
JavaScript o importarse en el explorador haciendo referencia a la *node_modules\ @aspnet\signalr-protocol-
msgpack\dist\browser\signalr-protocol-msgpack.js* archivo. En un explorador la msgpack5 también se debe hacer
referencia a la biblioteca. Use un <script> etiqueta que se va a crear una referencia. La biblioteca se puede
encontrar en node_modules\msgpack5\dist\msgpack5.js.

NOTE
Cuando se usa el <script> elemento, el orden es importante. Si msgpack.js de protocolo de signalr se hace referencia
antes de msgpack5.js, se produce un error al intentar conectarse con MessagePack. signalr.js también es necesaria antes de
msgpack.js de protocolo de signalr.

<script src="~/lib/signalr/signalr.js"></script>
<script src="~/lib/msgpack5/msgpack5.js"></script>
<script src="~/lib/signalr/signalr-protocol-msgpack.js"></script>

Agregar .withHubProtocol(new signalR.protocols.msgpack.MessagePackHubProtocol()) a la HubConnectionBuilder va a


configurar el cliente para utilizar el protocolo MessagePack al conectarse a un servidor.

const connection = new signalR.HubConnectionBuilder()


.withUrl("/chatHub")
.withHubProtocol(new signalR.protocols.msgpack.MessagePackHubProtocol())
.build();

NOTE
En este momento, no hay ninguna opción de configuración para el protocolo MessagePack en el cliente de JavaScript.

Recursos relacionados
Primeros pasos
Cliente .NET
Cliente de JavaScript
Publicar un núcleo de ASP.NET SignalR aplicación a
una aplicación Web de Azure
22/06/2018 • 3 minutes to read • Edit Online

Aplicación Web de Azure es un Microsoft la informática en nube servicio de plataforma para hospedar las
aplicaciones web, incluido ASP.NET Core.

NOTE
En este artículo se refiere a la publicación de una aplicación de ASP.NET Core SignalR desde Visual Studio. Visite SignalR
servicio para Azure para obtener más información sobre el uso de SignalR en Azure.

Publicar la aplicación
Visual Studio proporciona herramientas integradas para la publicación en una aplicación Web de Azure. Puede
usar el usuario de Visual Studio Code CLI de Azure comandos para publicar aplicaciones para Azure. Este
artículo trata la publicación con las herramientas de Visual Studio. Para publicar una aplicación mediante la CLI
de Azure, consulte publicar una aplicación de ASP.NET Core en Azure con las herramientas de línea de
comandos.
Haga doble clic en el proyecto en el Explorador de soluciones y seleccione publicar. Confirme que crear
nuevo está activada en la elegir un destino de publicación cuadro de diálogo y seleccione publicar.

Escriba la siguiente información en el crear servicio en la aplicación cuadro de diálogo y seleccione crear.
ELEMENTO DESCRIPCIÓN

Nombre de la aplicación Un nombre único de la aplicación.

Suscripción La suscripción de Azure que usa la aplicación.

Grupo de recursos El grupo de recursos relacionados a la que pertenece la


aplicación.

Plan de hospedaje El plan de precios de la aplicación web.

Visual Studio realiza las tareas siguientes:


Crea un perfil de publicación que contiene la configuración de publicación.
Crea o utiliza un archivo aplicación Web de Azure con los detalles proporcionados.
Publica la aplicación.
Inicia un explorador, con la aplicación web publicada cargada.
Tenga en cuenta el formato de la dirección URL para la aplicación es .azurewebsites {nombre de la aplicación}
.net. Por ejemplo, una aplicación denominada SignalRChattR tiene una dirección URL similar a
https://signalrchattr.azurewebsites.net .

Si se produce un error de HTTP 502.2, consulte versión de vista previa de implementar ASP.NET Core para el
servicio de aplicaciones de Azure para resolverlo.

Configurar la aplicación web de SignalR


Las aplicaciones ASP.NET SignalR Core que se publican como una aplicación Web de Azure debe tener ARR
afinidad habilitado. WebSockets debe habilitarse para permitir que el transporte de WebSockets a función.
En el portal de Azure, vaya a configuración de la aplicación para su aplicación web. Establecer WebSockets a
eny compruebe ARR afinidad es en.

WebSockets y otros transportes están limitados según el Plan de servicio de aplicaciones.

Recursos relacionados
Publicar una aplicación de ASP.NET Core en Azure con las herramientas de línea de comandos
Publicar una aplicación de ASP.NET Core en Azure con Visual Studio
Hospedar e implementar aplicaciones de ASP.NET Core Preview en Azure
Uso de ASP.NET Core SignalR transmisión por
secuencias
23/06/2018 • 4 minutes to read • Edit Online

Por Brennan Conroy


Núcleo de ASP.NET SignalR es compatible con transmisión por secuencias valores devueltos de métodos de
servidor. Esto es útil para escenarios donde fragmentos de datos aparecerá en con el tiempo. Cuando un valor
devuelto se transmite al cliente, significará que se ha enviado cada fragmento al cliente en cuanto se convierte en
disponible, en lugar de esperar a todos los datos hasta que esté disponible.
Vea o descargue el código de ejemplo (cómo descargarlo)

Configurar el concentrador
Un método de concentrador se convierte automáticamente en un método de concentrador de transmisión por
secuencias cuando devuelve un ChannelReader<T> o Task<ChannelReader<T>> . A continuación se muestra un
ejemplo que muestra los conceptos básicos de transmisión de datos al cliente. Cada vez que un objeto se escribe
en el ChannelReader ese objeto se envía inmediatamente al cliente. Al final, el ChannelReader se complete para
indicar al cliente la secuencia está cerrada.

NOTE
Escribir en el ChannelReader en un subproceso en segundo plano y vuelva la ChannelReader tan pronto como sea
posible. Otras las invocaciones del concentrador se bloqueará hasta que un ChannelReader se devuelve.

public class StreamHub : Hub


{
public ChannelReader<int> Counter(int count, int delay)
{
var channel = Channel.CreateUnbounded<int>();

// We don't want to await WriteItems, otherwise we'd end up waiting


// for all the items to be written before returning the channel back to
// the client.
_ = WriteItems(channel.Writer, count, delay);

return channel.Reader;
}

private async Task WriteItems(ChannelWriter<int> writer, int count, int delay)


{
for (var i = 0; i < count; i++)
{
await writer.WriteAsync(i);
await Task.Delay(delay);
}

writer.TryComplete();
}
}
Cliente .NET
El StreamAsChannelAsync método HubConnection se utiliza para invocar un método de transmisión por secuencias.
Pasar el nombre del método de concentrador y los argumentos definidos en el método de concentrador a
StreamAsChannelAsync . El parámetro genérico en StreamAsChannelAsync<T> especifica el tipo de objetos devueltos
por el método de transmisión por secuencias. Un ChannelReader<T> se devuelve desde la invocación de flujo y
representa el flujo en el cliente. Para leer los datos, es un patrón común para recorrer en WaitToReadAsync y llamar
a TryRead cuando los datos están disponibles. El bucle terminará cuando se ha cerrado la secuencia por el
servidor, o el token de cancelación que se pasa a StreamAsChannelAsync se cancela.

var channel = await hubConnection.StreamAsChannelAsync<int>("Counter", 10, 500, CancellationToken.None);

// Wait asynchronously for data to become available


while (await channel.WaitToReadAsync())
{
// Read all currently available data synchronously, before waiting for more data
while (channel.TryRead(out var count))
{
Console.WriteLine($"{count}");
}
}

Console.WriteLine("Streaming completed");

Cliente de JavaScript
Los clientes de JavaScript llamar a métodos de transmisión por secuencias en concentradores mediante
connection.stream . El stream método acepta dos argumentos:

El nombre del método de concentrador. En el ejemplo siguiente, el nombre de método de concentrador es


Counter .
Argumentos definidos en el método de concentrador. En el ejemplo siguiente, los argumentos son: un recuento
del número de elementos de flujo para recibir y el retraso entre los elementos de la secuencia.
connection.stream Devuelve un IStreamResult que contiene un subscribe método. Pasar un IStreamSubscriber a
subscribe y establezca el next , error , y complete las devoluciones de llamada para recibir notificaciones de la
stream invocación.

connection.stream("Counter", 10, 500)


.subscribe({
next: (item) => {
var li = document.createElement("li");
li.textContent = item;
document.getElementById("messagesList").appendChild(li);
},
complete: () => {
var li = document.createElement("li");
li.textContent = "Stream completed";
document.getElementById("messagesList").appendChild(li);
},
error: (err) => {
var li = document.createElement("li");
li.textContent = err;
document.getElementById("messagesList").appendChild(li);
},
});

Para finalizar la secuencia de la llamada del cliente la dispose método en el ISubscription que se devuelve desde
el subscribe método.

Recursos relacionados
Concentradores
Cliente .NET
Cliente de JavaScript
Publicar en Azure
Plataformas compatibles con ASP.NET Core SignalR
22/06/2018 • 2 minutes to read • Edit Online

Requisitos de sistema de servidor


SignalR para ASP.NET Core es compatible con cualquier plataforma de server Core de ASP.NET es compatible
con.

Requisitos del sistema cliente


Compatibilidad con exploradores
SignalR para el cliente de ASP.NET Core JavaScript admite los siguientes exploradores:

EXPLORADOR VERSIÓN

Microsoft Internet Explorer 11

Microsoft Edge actuales

Mozilla Firefox actuales

Google Chrome; incluye Android actuales

Safari; incluye iOS actuales

Compatibilidad con clientes de .NET


Cualquier plataforma de servidor compatible con ASP.NET Core. Cuando se usa IIS, el transporte de WebSockets
requiere IIS 8.0 o posterior, en Windows Server 2012 o posterior. Otros transportes se admiten en todas las
plataformas.
Desarrollo para dispositivos móviles con ASP.NET
Core
21/06/2018 • 2 minutes to read • Edit Online

Creación de servicios back-end para aplicaciones móviles nativas


Crear servicios back-end para aplicaciones móviles
nativas con ASP.NET Core
25/06/2018 • 14 minutes to read • Edit Online

Por Steve Smith


Las aplicaciones móviles pueden comunicarse fácilmente con servicios back-end de ASP.NET Core.
Ver o descargar código de ejemplo de servicios back-end

La aplicación móvil nativa de ejemplo


Este tutorial muestra cómo crear servicios back-end mediante ASP.NET Core MVC para admitir aplicaciones
móviles nativas. Usa la aplicación Xamarin Forms ToDoRest como su cliente nativo, lo que incluye clientes nativos
independientes para dispositivos Android, iOS, Windows Universal y Windows Phone. Puede seguir el tutorial
vinculado para crear la aplicación nativa (e instalar las herramientas de Xamarin gratuitas necesarias), así como
descargar la solución de ejemplo de Xamarin. El ejemplo de Xamarin incluye un proyecto de servicios de ASP.NET
Web API 2, que sustituye a las aplicaciones de ASP.NET Core de este artículo (sin cambios requeridos por el
cliente).
Características
La aplicación ToDoRest permite enumerar, agregar, eliminar y actualizar tareas pendientes. Cada tarea tiene un
identificador, un nombre, notas y una propiedad que indica si ya se ha realizado.
La vista principal de las tareas, como se muestra anteriormente, indica el nombre de cada tarea e indica si se ha
realizado con una marca de verificación.
Al pulsar el icono + se abre un cuadro de diálogo para agregar un elemento:

Al pulsar un elemento en la pantalla de la lista principal se abre un cuadro de diálogo de edición, donde se puede
modificar el nombre del elemento, las notas y la configuración de Done (Listo), o se puede eliminar el elemento:
Este ejemplo está configurado para usar de forma predeterminada servicios back-end hospedados en
developer.xamarin.com, lo que permite operaciones de solo lectura. Para probarlo usted mismo con la aplicación
de ASP.NET Core que creó en la siguiente sección que se ejecuta en el equipo, debe actualizar la constante
RestUrl de la aplicación. Vaya hasta el proyecto ToDoREST y abra el archivo Constants.cs. Reemplace RestUrl con
una dirección URL que incluya la dirección IP de su equipo (no localhost ni 127.0.0.1, puesto que esta dirección se
usa desde el emulador de dispositivo, no desde el equipo). Incluya también el número de puerto (5000). Para
comprobar que los servicios funcionan con un dispositivo, asegúrese de que no tiene un firewall activo que
bloquea el acceso a este puerto.

// URL of REST service (Xamarin ReadOnly Service)


//public static string RestUrl = "http://developer.xamarin.com:8081/api/todoitems{0}";

// use your machine's IP address


public static string RestUrl = "http://192.168.1.207:5000/api/todoitems/{0}";

Creación del proyecto de ASP.NET Core


Cree una aplicación web de ASP.NET Core en Visual Studio. Elija la plantilla de API web y Sin autenticación.
Denomine el proyecto ToDoApi.
La aplicación debería responder a todas las solicitudes realizadas al puerto 5000. Actualice Program.cs para que
incluya .UseUrls("http://*:5000") para conseguir lo siguiente:

var host = new WebHostBuilder()


.UseKestrel()
.UseUrls("http://*:5000")
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<Startup>()
.Build();

NOTE
Asegúrese de que ejecuta la aplicación directamente, en lugar de tras IIS Express, que omite las solicitudes no locales de
forma predeterminada. Ejecute dotnet run desde un símbolo del sistema o elija el perfil del nombre de aplicación en la lista
desplegable de destino de depuración en la barra de herramientas de Visual Studio.

Agregue una clase de modelo para representar las tareas pendientes. Marque los campos obligatorios mediante el
atributo [Required] :
using System.ComponentModel.DataAnnotations;

namespace ToDoApi.Models
{
public class ToDoItem
{
[Required]
public string ID { get; set; }

[Required]
public string Name { get; set; }

[Required]
public string Notes { get; set; }

public bool Done { get; set; }


}
}

Los métodos de API necesitan alguna manera de trabajar con los datos. Use la misma interfaz de
IToDoRepository que usa el ejemplo original de Xamarin:

using System.Collections.Generic;
using ToDoApi.Models;

namespace ToDoApi.Interfaces
{
public interface IToDoRepository
{
bool DoesItemExist(string id);
IEnumerable<ToDoItem> All { get; }
ToDoItem Find(string id);
void Insert(ToDoItem item);
void Update(ToDoItem item);
void Delete(string id);
}
}

En este ejemplo, la implementación usa solo una colección de elementos privada:

using System.Collections.Generic;
using System.Linq;
using ToDoApi.Interfaces;
using ToDoApi.Models;

namespace ToDoApi.Services
{
public class ToDoRepository : IToDoRepository
{
private List<ToDoItem> _toDoList;

public ToDoRepository()
{
InitializeData();
}

public IEnumerable<ToDoItem> All


{
get { return _toDoList; }
}

public bool DoesItemExist(string id)


{
return _toDoList.Any(item => item.ID == id);
return _toDoList.Any(item => item.ID == id);
}

public ToDoItem Find(string id)


{
return _toDoList.FirstOrDefault(item => item.ID == id);
}

public void Insert(ToDoItem item)


{
_toDoList.Add(item);
}

public void Update(ToDoItem item)


{
var todoItem = this.Find(item.ID);
var index = _toDoList.IndexOf(todoItem);
_toDoList.RemoveAt(index);
_toDoList.Insert(index, item);
}

public void Delete(string id)


{
_toDoList.Remove(this.Find(id));
}

private void InitializeData()


{
_toDoList = new List<ToDoItem>();

var todoItem1 = new ToDoItem


{
ID = "6bb8a868-dba1-4f1a-93b7-24ebce87e243",
Name = "Learn app development",
Notes = "Attend Xamarin University",
Done = true
};

var todoItem2 = new ToDoItem


{
ID = "b94afb54-a1cb-4313-8af3-b7511551b33b",
Name = "Develop apps",
Notes = "Use Xamarin Studio/Visual Studio",
Done = false
};

var todoItem3 = new ToDoItem


{
ID = "ecfa6f80-3671-4911-aabe-63cc442c1ecf",
Name = "Publish apps",
Notes = "All app stores",
Done = false,
};

_toDoList.Add(todoItem1);
_toDoList.Add(todoItem2);
_toDoList.Add(todoItem3);
}
}
}

Configure la implementación en Startup.cs:


public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddMvc();

services.AddSingleton<IToDoRepository,ToDoRepository>();
}

En este punto, está listo para crear el ToDoItemsController.

TIP
Obtenga más información sobre cómo crear API web en Cree su primera API web con ASP.NET Core MVC y Visual Studio.

Crear el controlador
Agregue un nuevo controlador para el proyecto: ToDoItemsController. Debe heredar de
Microsoft.AspNetCore.Mvc.Controller. Agregue un atributo Route para indicar que el controlador controlará las
solicitudes realizadas a las rutas de acceso que comiencen con api/todoitems . El token [controller] de la ruta se
sustituye por el nombre del controlador (si se omite el sufijo Controller ) y es especialmente útil para las rutas
globales. Obtenga más información sobre el enrutamiento.
El controlador necesita un IToDoRepository para funcionar. Solicite una instancia de este tipo a través del
constructor del controlador. En tiempo de ejecución, esta instancia se proporcionará con la compatibilidad del
marco con la inserción de dependencias.

using System;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using ToDoApi.Interfaces;
using ToDoApi.Models;

namespace ToDoApi.Controllers
{
[Route("api/[controller]")]
public class ToDoItemsController : Controller
{
private readonly IToDoRepository _toDoRepository;

public ToDoItemsController(IToDoRepository toDoRepository)


{
_toDoRepository = toDoRepository;
}

Esta API es compatible con cuatro verbos HTTP diferentes para realizar operaciones CRUD (creación, lectura,
actualización, eliminación) en el origen de datos. La más simple de ellas es la operación de lectura, que
corresponde a una solicitud HTTP GET.
Leer elementos
La solicitud de una lista de elementos se realiza con una solicitud GET al método List . El atributo [HttpGet] en
el método List indica que esta acción solo debe controlar las solicitudes GET. La ruta de esta acción es la ruta
especificada en el controlador. No es necesario usar el nombre de acción como parte de la ruta. Solo debe
asegurarse de que cada acción tiene una ruta única e inequívoca. El enrutamiento de atributos se puede aplicar
tanto a los niveles de controlador como de método para crear rutas específicas.
[HttpGet]
public IActionResult List()
{
return Ok(_toDoRepository.All);
}

El método List devuelve un código de respuesta 200 OK y todos los elementos de lista de tareas, serializados
como JSON.
Puede probar el nuevo método de API con una variedad de herramientas, como Postman, que se muestra a
continuación:

Crear elementos
Por convención, la creación de elementos de datos se asigna al verbo HTTP POST. El método Create tiene un
atributo [HttpPost] aplicado y acepta una instancia ToDoItem . Puesto que el argumento item se pasará en el
cuerpo de la solicitud POST, este parámetro se decora con el atributo [FromBody] .
Dentro del método, se comprueba la validez del elemento y si existió anteriormente en el almacén de datos y, si no
hay problemas, se agrega mediante el repositorio. Al comprobar ModelState.IsValid se realiza una validación de
modelos, y debe realizarse en cada método de API que acepte datos proporcionados por usuario.
[HttpPost]
public IActionResult Create([FromBody] ToDoItem item)
{
try
{
if (item == null || !ModelState.IsValid)
{
return BadRequest(ErrorCode.TodoItemNameAndNotesRequired.ToString());
}
bool itemExists = _toDoRepository.DoesItemExist(item.ID);
if (itemExists)
{
return StatusCode(StatusCodes.Status409Conflict, ErrorCode.TodoItemIDInUse.ToString());
}
_toDoRepository.Insert(item);
}
catch (Exception)
{
return BadRequest(ErrorCode.CouldNotCreateItem.ToString());
}
return Ok(item);
}

El ejemplo usa una enumeración que contiene códigos de error que se pasan al cliente móvil:

public enum ErrorCode


{
TodoItemNameAndNotesRequired,
TodoItemIDInUse,
RecordNotFound,
CouldNotCreateItem,
CouldNotUpdateItem,
CouldNotDeleteItem
}

Pruebe a agregar nuevos elementos con Postman eligiendo el verbo POST que proporciona el nuevo objeto en
formato JSON en el cuerpo de la solicitud. También debe agregar un encabezado de solicitud que especifica un
Content-Type de application/json .
El método devuelve el elemento recién creado en la respuesta.
Actualizar elementos
La modificación de registros se realiza mediante solicitudes HTTP PUT. Aparte de este cambio, el método Edit es
casi idéntico a Create . Tenga en cuenta que, si no se encuentra el registro, la acción Edit devolverá una
respuesta NotFound (404).
[HttpPut]
public IActionResult Edit([FromBody] ToDoItem item)
{
try
{
if (item == null || !ModelState.IsValid)
{
return BadRequest(ErrorCode.TodoItemNameAndNotesRequired.ToString());
}
var existingItem = _toDoRepository.Find(item.ID);
if (existingItem == null)
{
return NotFound(ErrorCode.RecordNotFound.ToString());
}
_toDoRepository.Update(item);
}
catch (Exception)
{
return BadRequest(ErrorCode.CouldNotUpdateItem.ToString());
}
return NoContent();
}

Para probar con Postman, cambie el verbo a PUT. Especifique los datos actualizados del objeto en el cuerpo de la
solicitud.

Este método devuelve una respuesta NoContent (204) cuando se realiza correctamente, para mantener la
coherencia con la API existente.
Eliminar elementos
La eliminación de registros se consigue mediante solicitudes DELETE al servicio y pasando el identificador del
elemento que va a eliminar. Al igual que con las actualizaciones, las solicitudes para elementos que no existen
recibirán respuestas NotFound . De lo contrario, las solicitudes correctas recibirán una respuesta NoContent (204).

[HttpDelete("{id}")]
public IActionResult Delete(string id)
{
try
{
var item = _toDoRepository.Find(id);
if (item == null)
{
return NotFound(ErrorCode.RecordNotFound.ToString());
}
_toDoRepository.Delete(id);
}
catch (Exception)
{
return BadRequest(ErrorCode.CouldNotDeleteItem.ToString());
}
return NoContent();
}

Tenga en cuenta que, al probar la funcionalidad de eliminar, no se necesita nada en el cuerpo de la solicitud.

Convenciones comunes de Web API


Al desarrollar los servicios back-end de la aplicación, necesitará acceder a un conjunto coherente de convenciones
o directivas para controlar cuestiones transversales. Por ejemplo, en el servicio mostrado anteriormente, las
solicitudes de registros específicos que no se encontraron recibieron una respuesta NotFound , en lugar de una
respuesta BadRequest . De forma similar, los comandos realizados a este servicio que pasaron en tipos enlazados a
un modelo siempre se comprobaron como ModelState.IsValid y devolvieron una BadRequest para los tipos de
modelos no válidos.
Después de identificar una directiva común para las API, normalmente puede encapsularla en un filtro. Obtenga
más información sobre cómo encapsular directivas de API comunes en aplicaciones de ASP.NET Core MVC.
Hospedaje e implementación de ASP.NET Core
21/06/2018 • 7 minutes to read • Edit Online

En general, para implementar una aplicación de ASP.NET Core en un entorno de hospedaje:


Publique la aplicación en una carpeta del servidor de hospedaje.
Configure un administrador de procesos que inicie la aplicación cuando lleguen las solicitudes y la reinicie
si se bloquea o si se reinicia el servidor.
Si desea la configuración de un proxy inverso, configure un proxy inverso que reenvíe las solicitudes a la
aplicación.

Publicar la aplicación en una carpeta


El comando de la CLI dotnet publish compila el código de la aplicación y copia los archivos necesarios para
ejecutar la aplicación en una carpeta publish. Al efectuar una implementación desde Visual Studio, el paso del
comando dotnet publish se produce automáticamente antes de que los archivos se copien en el destino de
implementación.
Contenido de la carpeta
La carpeta publish contiene archivos .exe y .dll de la aplicación, sus dependencias y, de forma opcional, el
tiempo de ejecución .NET.
Se puede publicar una aplicación .NET Core como independiente o dependiente del marco. Si la aplicación es
independiente, los archivos .dll que contienen el tiempo de ejecución .NET se incluyen en la carpeta publish. Si
la aplicación es dependiente del marco, los archivos del tiempo de ejecución .NET no se incluyen porque la
aplicación tiene una referencia a una versión de .NET que está instalada en el servidor. El modelo de
implementación predeterminado es dependiente del marco. Para más información, vea Implementación de
aplicaciones .NET Core.
Además de los archivos .exe y .dll, la carpeta publish de una aplicación ASP.NET Core suele contener archivos
de configuración, recursos estáticos y vistas de MVC. Para más información, vea Directory structure
(Estructura de directorios).

Configurar un administrador de procesos


Una aplicación de ASP.NET Core es una aplicación de consola que se debe iniciar cuando se inicia un servidor
y se debe reiniciar si este se bloquea. Para automatizar los inicios y los reinicios, se necesita un administrador
de procesos. Los administradores de procesos más comunes para ASP.NET Core son:
Linux
Nginx
Apache
Windows
IIS
Servicio de Windows

Configurar un proxy inverso


ASP.NET Core 2.x
ASP.NET Core 1.x
Si la aplicación usa el servidor web Kestrel, puede usar Nginx, Apache o IIS como servidor proxy inverso. Un
servidor proxy inverso recibe las solicitudes HTTP de Internet y las reenvía a Kestrel después de un control
preliminar.
Cualquiera de las configuraciones—con o sin un servidor proxy inverso—es una configuración de hospedaje
válida y admitida para ASP.NET 2.0 o aplicaciones posteriores. Para más información, vea When to use Kestrel
with a reverse proxy (Cuándo se debe usar Kestrel con un proxy inverso).

Escenarios de servidor proxy y equilibrador de carga


Podría ser necesario realizar una configuración adicional para las aplicaciones hospedadas detrás de
servidores proxy y equilibradores de carga. Sin una configuración adicional, una aplicación podría no tener
acceso al esquema (HTTP/HTTPS ) y la dirección IP remota donde se originó una solicitud. Para más
información, vea Configurar ASP.NET Core para trabajar con servidores proxy y equilibradores de carga.

Usar Visual Studio y MSBuild para automatizar la implementación


La implementación a menudo requiere tareas adicionales además de copiar el resultado del comando dotnet
publish en un servidor. Por ejemplo, podrían necesitarse o eliminarse archivos adicionales de la carpeta
publish. Para la implementación web, Visual Studio usa MSBuild, que puede personalizar de modo que lleve a
cabo muchas otras tareas durante la implementación. Para más información, vea Publish profiles in Visual
Studio (Publicar perfiles en Visual Studio) y el libro Using MSBuild and Team Foundation Build (Usar
MSBuild y Team Foundation Build).
Mediante la característica de publicación web o la compatibilidad integrada con Git, puede implementar las
aplicaciones directamente desde Visual Studio en Azure App Service. Visual Studio Team Services es
compatible con la implementación continua en Azure App Service.

Publicación en Azure
Vea Publicar una aplicación web de ASP.NET Core en Azure App Service con Visual Studio para obtener
instrucciones sobre cómo publicar una aplicación en Azure con Visual Studio. También se puede publicar la
aplicación en Azure desde la línea de comandos.

Recursos adicionales
Para información sobre cómo usar Docker como entorno de hospedaje, vea Host ASP.NET Core apps in
Docker (Hospedar aplicaciones de ASP.NET Core en Docker).
Hospedaje de ASP.NET Core en Azure App Service
10/05/2018 • 12 minutes to read • Edit Online

Azure App Service es un servicio de plataforma de informática en la nube de Microsoft que sirve para hospedar
aplicaciones web, como ASP.NET Core.

Recursos útiles
La documentación de Web Apps de Azure es un recurso que incluye documentación, tutoriales, ejemplos, guías de
procedimientos y otros recursos de aplicaciones de Azure. Dos tutoriales importantes que pertenecen al
hospedaje de aplicaciones de ASP.NET Core son:
Inicio rápido: Crear una aplicación web de ASP.NET Core en Azure
Usar Visual Studio para crear e implementar una aplicación web de ASP.NET Core para Azure App Service en
Windows.
Inicio rápido: Crear una aplicación web de .NET Core en App Service en Linux
Usar la línea de comandos para crear e implementar una aplicación web de ASP.NET Core para Azure App Service
en Linux.
Los artículos siguientes están disponibles en la documentación de ASP.NET Core:
Publicación en Azure con Visual Studio
Obtenga información sobre cómo publicar una aplicación de ASP.NET Core en Azure App Service con Visual
Studio.
Publicación en Azure con herramientas de CLI
Obtenga información sobre cómo publicar una aplicación de ASP.NET Core en Azure App Service con el cliente de
línea de comandos de Git.
Implementación continua en Azure con Visual Studio y Git
Obtenga información sobre cómo crear una aplicación web de ASP.NET Core con Visual Studio e implementarla
en Azure App Service con Git para una implementación continua.
Implementación continua en Azure con VSTS
Configure una compilación de integración continua para una aplicación de ASP.NET Core y, después, cree una
versión de implementación continua para Azure App Service.
Espacio aislado de Azure Web App
Detecte limitaciones de ejecución en tiempo de ejecución de Azure App Service aplicadas por la plataforma de
aplicaciones de Azure.

Configuración de aplicación
En ASP.NET Core 2.0 y versiones posteriores, tres paquetes del metapaquete Microsoft.AspNetCore.All
proporcionan características de registro automático para aplicaciones implementadas en Azure App Service:
Microsoft.AspNetCore.AzureAppServices.HostingStartup utiliza IHostingStartup para proporcionar a ASP.NET
Core integración iluminada con Azure App Service. El paquete
Microsoft.AspNetCore.AzureAppServicesIntegration proporciona las características de registro agregadas.
Microsoft.AspNetCore.AzureAppServicesIntegration ejecuta AddAzureWebAppDiagnostics para agregar a
Azure App Service proveedores de registro de diagnósticos en el paquete
Microsoft.Extensions.Logging.AzureAppServices.
Microsoft.Extensions.Logging.AzureAppServices proporciona implementaciones de registrador para admitir
registros de diagnósticos de Azure App Service y características de transmisión en secuencias de registro.

Escenarios de servidor proxy y equilibrador de carga


El software intermedio de integración con IIS, que configura el software intermedio de encabezados reenviados, y
el módulo de ASP.NET Core están configurados para reenviar el esquema (HTTP/HTTPS ) y la dirección IP remota
donde se originó la solicitud. Podría ser necesario realizar una configuración adicional para las aplicaciones
hospedadas detrás de servidores proxy y equilibradores de carga adicionales. Para más información, vea
Configurar ASP.NET Core para trabajar con servidores proxy y equilibradores de carga.

Supervisión y registro
Para obtener información sobre supervisión, registro y solución de problemas, consulte los artículos siguientes:
Cómo: Supervisar aplicaciones en Azure App Service
Obtenga información sobre cómo revisar las cuotas y las métricas para las aplicaciones y los planes de App
Service.
Habilitar el registro de diagnósticos para las aplicaciones web en Azure App Service
Descubra cómo habilitar y acceder a registro de diagnóstico para los códigos de estado HTTP, solicitudes con error
y actividad del servidor web.
Introducción a control de errores en ASP.NET Core
Comprender los métodos comunes para controlar los errores en las aplicaciones de ASP.NET Core.
Solución de problemas de ASP.NET Core en Azure App Service
Obtenga información sobre cómo diagnosticar problemas con las implementaciones de Azure App Service con las
aplicaciones de ASP.NET Core.
Referencia de errores comunes de Azure App Service e IIS con ASP.NET Core
Consulte los errores comunes de configuración de implementación para las aplicaciones hospedadas por Azure
App Service/IIS con consejos de solución de problemas.

Anillo de clave de protección de datos y ranuras de implementación


Las claves de protección de datos se conservan en la carpeta %HOME%\ASP.NET\DataProtection-Keys. Esta
carpeta está respaldada por el almacenamiento de red y se sincroniza en todas las máquinas que hospedan la
aplicación. Las claves no están protegidas en reposo. Esta carpeta proporciona el anillo de clave a todas las
instancias de una aplicación en una única ranura de implementación. Las ranuras de implementación
independientes, por ejemplo, almacenamiento provisional y producción, no comparten ningún anillo de clave.
Al realizar un intercambio entre ranuras de implementación, cualquier sistema que utilice la protección de datos
no podrá descifrar los datos almacenados mediante el anillo de clave situado dentro de la ranura anterior. El
middleware de cookies de ASP.NET usa protección de datos para proteger sus cookies. Esto hace que los usuarios
cierren sesión en una aplicación que usa el middleware de cookies de ASP.NET estándar. Para una solución de
anillo de clave independiente de la ranura, utilice un proveedor de anillo de clave externo, como estos:
Azure Blob Storage
Azure Key Vault
Almacén SQL
Redis Cache
Para obtener más información, consulte Proveedores de almacenamiento de claves.
Implementar una versión preliminar de ASP.NET Core en Azure App
Service
Las aplicaciones de versión preliminar de ASP.NET Core se pueden implementar en Azure App Service con los
procedimientos siguientes:
Instalación de la extensión de sitio de versión preliminar
Implementación de la aplicación independiente
Usar Docker con Web Apps para contenedores
Si tiene algún problema al usar la extensión de sitio de versión preliminar, abra una incidencia en GitHub.
Instalación de la extensión de sitio de versión preliminar
En Azure Portal, vaya a la hoja de App Service.
Escriba "ex" en el cuadro de búsqueda.
Seleccione Extensiones.
Seleccione "Agregar".

Seleccione ASP.NET Core 2.1 (x86) Runtime o ASP.NET Core 2.1 (x64) Runtime.
Seleccione Aceptar. Vuelva a seleccionar Aceptar.
Cuando se complete la operación de adición, se instalará la versión preliminar de .NET Core 2.1. Para comprobar
la instalación, ejecute dotnet --info en la consola. En la hoja de App Service:
Escriba "con" en el cuadro de búsqueda.
Seleccione Consola.
Escriba dotnet --info en la consola.
La imagen anterior se capturó en el momento en que se escribía esto. Podría ver una versión diferente.
dotnet --info muestra la ruta de acceso a la extensión de sitio en la que se ha instalado la versión preliminar.
Muestra que la aplicación se está ejecutando desde la extensión de sitio, en lugar de desde la ubicación
predeterminada ProgramFiles. Si ve ProgramFiles, reinicie el sitio y ejecute dotnet --info .
Uso de la extensión de sitio de versión preliminar con una plantilla de ARM
Si usa una plantilla de ARM para crear e implementar aplicaciones, puede usar el tipo de recurso siteextensions
para agregar la extensión de sitio a una aplicación web. Por ejemplo:

{
"type": "siteextensions",
"name": "AspNetCoreRuntime",
"apiVersion": "2015-04-01",
"location": "[resourceGroup().location]",
"properties": {
"version": "[parameters('aspnetcoreVersion')]"
},
"dependsOn": [
"[resourceId('Microsoft.Web/Sites', parameters('siteName'))]"
]
}

Implementación de la aplicación independiente


Puede implementar una aplicación independiente que contenga la versión preliminar del entorno de ejecución. Al
implementar una aplicación independiente:
No es necesario que el sitio esté preparado.
La aplicación se debe publicar de forma diferente en relación con una implementación que dependa de un
marco, con un entorno de ejecución compartido y el servidor como host.
Las aplicaciones independientes son una opción válida para todas las aplicaciones ASP.NET Core.
Usar Docker con Web Apps para contenedores
Docker Hub contiene las imágenes de Docker de versión preliminar 2.1 más recientes. Las imágenes se pueden
usar como base. Use la imagen y efectúe la implementación en Web App for Containers con normalidad.

Recursos adicionales
Introducción a Web Apps (vídeo de introducción de 5 minutos)
Azure App Service: el mejor lugar para hospedar las aplicaciones .NET (vídeo de introducción de 55 minutos)
Azure Friday: experiencia de diagnóstico y solución de problemas de Azure App Service (vídeo de 12 minutos)
Introducción a diagnósticos de Azure App Service
Azure App Service en Windows Server utiliza Internet Information Services (IIS ). Los temas siguientes se aplican
a la tecnología subyacente de IIS:
Hospedaje de ASP.NET Core en Windows con IIS
Introducción al módulo ASP.NET Core
Referencia de configuración del módulo ASP.NET Core
Módulos de IIS con ASP.NET Core
Biblioteca de TechNet de Microsoft: Windows Server
Publicar una aplicación de ASP.NET Core en Azure
con Visual Studio
25/06/2018 • 7 minutes to read • Edit Online

De Rick Anderson, Cesar Blum Silveira y Rachel Appel

IMPORTANT
Aviso sobre el uso de las versiones preliminares de ASP.NET Core 2.1
Consulte Implementar una versión preliminar de ASP.NET Core en Azure App Service.

Vea Publish to Azure from Visual Studio for Mac (Publicación en Azure desde Visual Studio para Mac) si
trabaja en un equipo macOS.
Para solucionar un problema con la implementación de App Service, vea Troubleshoot ASP.NET Core on Azure
App Service (Solucionar problemas de ASP.NET Core en Azure App Service).

Configurar
Abra una cuenta gratuita de Azure si no tiene una.

Creación de una aplicación web


En la página de inicio de Visual Studio, seleccione Archivo > Nuevo > Proyecto....

Rellene el cuadro de diálogo Nuevo proyecto:


En el panel izquierdo, seleccione .NET Core.
En el panel central, seleccione Aplicación web de ASP.NET Core.
Seleccione Aceptar.

En el cuadro de diálogo Nueva aplicación web de ASP.NET Core, haga lo siguiente:


Seleccione Aplicación web.
Seleccione Cambiar autenticación.

Se mostrará el cuadro de diálogo Cambiar autenticación.


Seleccione Cuentas de usuario individuales.
Seleccione Aceptar para volver a la nueva aplicación web de ASP.NET Core y vuelva a seleccionar
Aceptar.

Visual Studio crea la solución.

Ejecutar la aplicación
Presione CTRL+F5 para ejecutar el proyecto.
Pruebe los vínculos Acerca de y Contacto.
Registrar un usuario
Seleccione Registrar y registre a un usuario nuevo. Puede usar una dirección de correo electrónico
ficticia. Al enviar la información, la página mostrará el error siguiente:
"Error interno del servidor: Error en una operación de base de datos al procesar la solicitud. Excepción
de SQL: No se puede abrir la base de datos. La aplicación de las migraciones existentes para el contexto
de base de datos de la aplicación puede solucionar este problema".
Seleccione Aplicar migraciones y, una vez actualizada la página, vuelva a cargarla.

La aplicación muestra el correo electrónico usado para registrar al nuevo usuario y el vínculo Cerrar sesión.
Implementación de la aplicación en Azure
Desde el Explorador de soluciones, haga clic con el botón derecho en el proyecto y seleccione Publicar...
En el cuadro de diálogo Publicar:
Seleccione Microsoft Azure App Service.
Seleccione el icono de engranaje y luego Crear perfil.
Seleccione Crear perfil.
Crear recursos de Azure
Aparece el cuadro de diálogo Crear servicio de aplicaciones:
Especifique la suscripción.
Se rellenan los campos de entrada Nombre de la aplicación, Grupo de recursos y Plan de App
Service. Puede mantener estos nombres o cambiarlos.
Seleccione la pestaña Servicios para crear una base de datos.
Seleccione el icono verde + para crear una instancia de SQL Database.

Seleccione Nuevo... en el cuadro de diálogo Configurar SQL Database para crear una base de datos.
Se mostrará el cuadro de diálogo Configurar SQL Server.
Escriba un nombre de usuario y una contraseña de administrador y seleccione Aceptar. Puede conservar el
nombre de servidor predeterminado.

NOTE
No se permite "admin" como nombre de usuario de administrador.
Seleccione Aceptar.
Visual Studio volverá al cuadro de diálogo Crear un servicio de App Service.
Seleccione Crear en el cuadro de diálogo Crear un servicio de App Service.

Visual Studio crea la aplicación web y SQL Server en Azure. Este paso puede llevar varios minutos. Para más
información sobre los recursos creados, vea Recursos adicionales.
Cuando termine la implementación, seleccione Configuración:

En la página Configuración del cuadro de diálogo Publicar, haga lo siguiente:


Expanda Bases de datos y active Usar esta cadena de conexión en tiempo de ejecución.
Expanda Migraciones de Entity Framework y active Aplicar esta migración al publicar.
Seleccione Guardar. Visual Studio volverá al cuadro de diálogo Publicar.
Haga clic en Publicar. Visual Studio publica la aplicación en Azure. Cuando la implementación termine, la
aplicación se abre en un explorador.
Prueba de la aplicación en Azure
Pruebe los vínculos Acerca de y Contacto.
Registre un nuevo usuario.
Actualización de la aplicación
Edite la página de Razor Pages/About.cshtml y modifique su contenido. Por ejemplo, puede modificar el
párrafo para que diga "¡ Hola, ASP.NET Core!": [!code-htmlAbout]
Haga clic con el botón derecho sobre el proyecto y vuelva a seleccionar Publicar....
Una vez publicada la aplicación, compruebe que los cambios realizados estén disponibles en Azure.
Limpieza
Cuando haya terminado de probar la aplicación, vaya a Azure Portal y elimínela.
Seleccione Grupos de recursos y, luego, el grupo de recursos que haya creado.

En la página Grupos de recursos, seleccione Eliminar.


Escriba el nombre del grupo de recursos y seleccione Eliminar. La aplicación y todos los demás recursos
que ha creado en este tutorial se han eliminado de Azure.
Pasos siguientes
Implementación continua en Azure con Visual Studio y Git

Recursos adicionales
Azure App Service
Grupos de recursos de Azure
Azure SQL Database
Solución de problemas de ASP.NET Core en Azure App Service
Publicar una aplicación ASP.NET Core en Azure con
las herramientas de línea de comandos
14/05/2018 • 5 minutes to read • Edit Online

Por Cam Soper

IMPORTANT
Aviso sobre el uso de las versiones preliminares de ASP.NET Core 2.1
Consulte Implementar una versión preliminar de ASP.NET Core en Azure App Service.

En este tutorial se explica cómo compilar e implementar una aplicación de ASP.NET Core en Microsoft Azure
App Service con las herramientas de línea de comandos. Cuando termine, tendrá una aplicación web integrada
en ASP.NET MVC Core hospedada como una aplicación web de Azure App Service. Este tutorial se escribe con
herramientas de línea de comandos de Windows, pero también puede aplicarse a entornos de macOS y Linux.
En este tutorial aprenderá a:
Crear un sitio web en Azure App Service con la CLI de Azure
Implementar una aplicación de ASP.NET Core en Azure App Service con la herramienta de línea de
comandos de Git

Requisitos previos
Para completar este tutorial, necesita:
Una suscripción de Microsoft Azure
.NET Core SDK 2.0 or later
Un cliente de línea de comandos de Git

Creación de una aplicación web


Cree un directorio para la aplicación web, cree una aplicación de ASP.NET Core MVC y luego ejecute el sitio
web de forma local.
Windows
Otros problemas

REM Create a new ASP.NET Core MVC application


dotnet new razor -o MyApplication

REM Change to the new directory that was just created


cd MyApplication

REM Run the application


dotnet run
Navegue a http://localhost:5000 para probar la aplicación.

Creación de la instancia de Azure App Service


Con Azure Cloud Shell, cree un grupo de recursos, un plan de App Service y una aplicación web de App
Service.
# Generate a unique Web App name
let randomNum=$RANDOM*$RANDOM
webappname=tutorialApp$randomNum

# Create the DotNetAzureTutorial resource group


az group create --name DotNetAzureTutorial --location EastUS

# Create an App Service plan.


az appservice plan create --name $webappname --resource-group DotNetAzureTutorial --sku FREE

# Create the Web App


az webapp create --name $webappname --resource-group DotNetAzureTutorial --plan $webappname

Antes de la implementación, defina las credenciales de implementación a nivel de cuenta con el siguiente
comando:

az webapp deployment user set --user-name <desired user name> --password <desired password>

Se necesita una dirección URL de implementación para implementar la aplicación con Git. Recupere una
dirección URL como esta.

az webapp deployment source config-local-git -n $webappname -g DotNetAzureTutorial --query [url] -o tsv

Anote la dirección URL mostrada que termina en .git . Se utiliza en el paso siguiente.

Implementación de la aplicación con Git


Está listo para implementar desde el equipo local mediante Git.

NOTE
Es seguro pasar por alto las advertencias de Git sobre los finales de línea.

Windows
Otros problemas

REM Initialize the local Git repository


git init

REM Add the contents of the working directory to the repo


git add --all

REM Commit the changes to the local repo


git commit -a -m "Initial commit"

REM Add the URL as a Git remote repository


git remote add azure <THE GIT URL YOU NOTED EARLIER>

REM Push the local repository to the remote


git push azure master

Git solicitará las credenciales de implementación establecidas anteriormente. Tras la autenticación, la aplicación
se inserta en la ubicación remota, se compila y se implementa.
Probar la aplicación
Navegue a https://<web app name>.azurewebsites.net para probar la aplicación. Para mostrar la dirección en
Cloud Shell o en la CLI de Azure, se usa lo siguiente:

az webapp show -n $webappname -g DotNetAzureTutorial --query defaultHostName -o tsv

Limpieza
Cuando termine de probar la aplicación y de inspeccionar el código y los recursos, elimine la aplicación web y el
plan con la eliminación del grupo de recursos.

az group delete -n DotNetAzureTutorial


Pasos siguientes
En este tutorial ha aprendido a:
Crear un sitio web en Azure App Service con la CLI de Azure
Implementar una aplicación de ASP.NET Core en Azure App Service con la herramienta de línea de
comandos de Git
A continuación, aprenda a usar la línea de comandos para implementar una aplicación web existente que usa
Cosmos DB.
Implementación en Azure desde la línea de comandos con .NET Core
Implementación continua en Azure con Visual Studio
y Git con ASP.NET Core
25/06/2018 • 13 minutes to read • Edit Online

Por Erik Reitan

IMPORTANT
Aviso sobre el uso de las versiones preliminares de ASP.NET Core 2.1
Consulte Implementar una versión preliminar de ASP.NET Core en Azure App Service.

En este tutorial se muestra cómo crear una aplicación web de ASP.NET Core con Visual Studio e implementarla
desde Visual Studio en Azure App Service mediante una implementación continua.
Vea también Use VSTS to Build and Publish to an Azure Web App with Continuous Deployment (Usar VSTS
para crear y publicar una aplicación web de Azure con la implementación continua), donde se muestra cómo
configurar un flujo de trabajo de una entrega continua (CD ) para Azure App Service con Visual Studio Team
Services. La entrega continua de Azure en Team Services simplifica la configuración de una canalización de
implementación sólida para publicar actualizaciones de aplicaciones hospedadas en Azure App Service. La
canalización se puede configurar desde Azure Portal para crear y ejecutar pruebas, implementarlas en un espacio
de ensayo y luego implementarlas en un entorno de producción.

NOTE
Para realizar este tutorial, necesita una cuenta de Microsoft Azure. Para obtener una, active las ventajas de suscriptor de
MSDN o regístrese para una prueba gratuita.

Requisitos previos
En este tutorial se da por hecho que está instalado el siguiente software:
Visual Studio
.NET Core SDK 2.0 or later
Git para Windows

Crear una aplicación web de ASP.NET Core


1. Inicie Visual Studio.
2. En el menú Archivo, seleccione Nuevo > Proyecto.
3. Seleccione la plantilla de proyecto Aplicación web ASP.NET Core. Aparece en Instalados > Plantillas
> Visual C# > .NET Core. Dé un nombre al proyecto SampleWebAppDemo . Seleccione la opción Crear
nuevo repositorio de Git y haga clic en Aceptar.
4. En el cuadro de diálogo Nuevo proyecto ASP.NET Core, seleccione la plantilla Vacía de ASP.NET Core y
haga clic en Aceptar.

NOTE
La versión más reciente de .NET Core es la 2.0.

Ejecutar la aplicación web de forma local


1. Cuando Visual Studio haya acabado de crear la aplicación, ejecútela seleccionando Depurar > Iniciar
depuración. Como alternativa, presione F5.
Visual Studio y la aplicación nueva pueden tardar un poco en inicializarse. Una vez completada la
operación, el explorador muestra la aplicación en ejecución.

2. Después de revisar la aplicación web en ejecución, cierre el explorador y seleccione el icono "Detener
depuración" de la barra de herramientas de Visual Studio para detener la aplicación.

Crear una aplicación web en Azure Portal


Los pasos siguientes le permiten crear una aplicación web en Azure Portal:
1. Inicie sesión en Azure Portal.
2. Seleccione Nuevo en la parte superior izquierda de la interfaz del portal.
3. Seleccione Web y móvil > Aplicación web.
4. En la hoja Aplicación web, escriba un valor único para el nombre del servicio de aplicaciones.
NOTE
El nombre de App Service debe ser único. El portal aplica esta regla cuando se proporciona el nombre. Si
proporciona un valor diferente, sustituya ese valor por cada aparición de SampleWebAppDemo en este tutorial.

También en la hoja Aplicación web, seleccione un plan o ubicación existente de App Service o bien cree
uno. Si va a crear un plan, seleccione el plan de tarifa, la ubicación y otras opciones. Para más información
sobre los planes de App Service, consulte Introducción detallada a los planes de Azure App Service.
5. Seleccione Crear. Azure aprovisionará e iniciará la aplicación web.
Habilitar la publicación de Git para la nueva aplicación web
Git es un sistema distribuido de control de versiones que se puede usar para implementar una aplicación web de
Azure App Service. El código de aplicación web se almacena en un repositorio de Git local y se implementa en
Azure mediante la inserción en un repositorio remoto.
1. Inicie sesión en Azure Portal.
2. Seleccione App Services para ver una lista los servicios de aplicaciones asociados a su suscripción de
Azure.
3. Seleccione la aplicación web que creó en la sección anterior de este tutorial.
4. En la hoja Implementación, seleccione Opciones de implementación > Elegir origen > Repositorio
de Git local.

5. Seleccione Aceptar.
6. Si no ha configurado antes las credenciales de implementación para publicar una aplicación web u otra
aplicación de App Service, configúrelas ahora:
Seleccione Configuración > Credenciales de implementación. Se muestra la hoja Configurar
credenciales de implementación.
Cree un nombre de usuario y una contraseña. Guarde la contraseña; la necesitará más adelante al
configurar Git.
Seleccione Guardar.
7. En la hoja Aplicación web, seleccione Configuración > Propiedades. La dirección URL del repositorio
de Git remoto en el que va a efectuar la implementación aparece en Dirección URL de Git.
8. Copie el valor de Dirección URL de Git para usarlo más adelante en el tutorial.

Publicación de la aplicación web en Azure App Service


En esta sección, creará un repositorio de Git local con Visual Studio y lo insertará desde ese repositorio en Azure
para implementar la aplicación web. Los pasos son los siguientes:
Agrega la configuración de repositorio remoto mediante el valor de dirección URL de GIT, de modo que el
repositorio local se pueda implementar en Azure
Confirmar los cambios en el proyecto
Insertar los cambios en el proyecto desde el repositorio local hasta el repositorio remoto en Azure
1. En el Explorador de soluciones, haga clic con el botón derecho en Solución 'SampleWebAppDemo' y
seleccione Confirmar. Se muestra Team Explorer.

2. En Team Explorer, seleccione Inicio (icono Inicio) > Configuración > Configuración del repositorio.
3. En la sección Remotos de Configuración del repositorio, seleccione Agregar. Aparece el cuadro de
diálogo Agregar remoto.
4. Establezca el nombre del repositorio remoto en Azure-SampleApp.
5. Establezca el valor de Recuperar en la dirección URL de Git que copió de Azure anteriormente en este
tutorial. Tenga en cuenta que esta es la dirección URL que termina en .git.

NOTE
Como alternativa, especifique el repositorio remoto desde la ventana de comandos. Para ello, abra la ventana de
comandos, cambie al directorio del proyecto y escriba el comando. Ejemplo:
git remote add Azure-SampleApp https://me@sampleapp.scm.azurewebsites.net:443/SampleApp.git
6. Seleccione Inicio (icono de Inicio) > Configuración > Configuración global. Confirme que se
establecen el nombre y la direcciones de correo electrónico. Seleccione Actualizar si es necesario.
7. Seleccione Inicio > Cambios para volver a la vista Cambios.
8. Escriba un mensaje de confirmación, como Inserción inicial 1 y seleccione Confirmar. Esta acción crea
una confirmación localmente.

NOTE
Como alternativa, puede confirmar los cambios desde la ventana de comandos. Para ello, abra la ventana de
comandos, cambie al directorio del proyecto y escriba los comandos de Git. Ejemplo:
git add .

git commit -am "Initial Push #1"

9. Seleccione Inicio > Sincronizar > Acciones > Abrir símbolo del sistema. El símbolo del sistema se
abre en el directorio del proyecto.
10. Escriba el siguiente comando en la ventana de comandos:
git push -u Azure-SampleApp master

11. Escriba la contraseña de sus credenciales de implementación de Azure que creó anteriormente en
Azure.
Este comando inicia el proceso de inserción de los archivos locales del proyecto en Azure. La salida del
comando anterior finaliza con un mensaje que indica que la implementación se efectuó correctamente.

remote: Finished successfully.


remote: Running post deployment command(s)...
remote: Deployment successful.
To https://username@samplewebappdemo01.scm.azurewebsites.net:443/SampleWebAppDemo01.git
* [new branch] master -> master
Branch master set up to track remote branch master from Azure-SampleApp.
NOTE
Si se requiere la colaboración en el proyecto, considere la posibilidad de insertarlos en GitHub antes de insertarlos
en Azure.

Comprobar la implementación activa


Compruebe que la transferencia de la aplicación web desde el entorno local a Azure es correcta.
En Azure Portal, seleccione la aplicación web. Después, seleccione Implementación > Opciones de
implementación.

Ejecutar la aplicación en Azure


Ahora que la aplicación web se ha implementado en Azure, ejecute la aplicación.
Esto puede realizarse de dos maneras:
En Azure Portal, busque la hoja de la aplicación web. Seleccione aminar para ver la aplicación en el explorador
predeterminado.
Abra un explorador y escriba la dirección URL de la aplicación web. Ejemplo:
http://SampleWebAppDemo.azurewebsites.net

Actualizar la aplicación web y volver a publicarla


Después de realizar cambios en el código local, vuelva a publicar la aplicación:
1. En el Explorador de soluciones de Visual Studio, abra el archivo Startup.cs.
2. En el método Configure , modifique el método Response.WriteAsync para que aparezca del siguiente
modo:

await context.Response.WriteAsync("Hello World! Deploy to Azure.");

3. Guarde los cambios en Startup.cs.


4. En el Explorador de soluciones, haga clic con el botón derecho en Solución 'SampleWebAppDemo' y
seleccione Confirmar. Se muestra Team Explorer.
5. Escriba un mensaje de confirmación, como Update #2 .
6. Presione el botón Confirmar para confirmar los cambios del proyecto.
7. Seleccione Inicio > Sincronizar > Acciones > Inserción.

NOTE
Como alternativa, inserte los cambios desde la ventana de comandos. Para ello, abra la ventana de comandos, cambie al
directorio del proyecto y escriba un comando de Git. Ejemplo:
git push -u Azure-SampleApp master

Ver la aplicación web actualizada en Azure


Para ver la aplicación web actualizada, seleccione Examinar en la hoja de la aplicación web en Azure Portal o
abra un explorador y escriba la dirección URL de la aplicación web. Ejemplo:
http://SampleWebAppDemo.azurewebsites.net

Recursos adicionales
Use VSTS to Build and Publish to an Azure Web App with Continuous Deployment (Uso de VSTS para
compilar y publicar una aplicación web de Azure con la implementación continua)
Proyecto Kudu
Solución de problemas de ASP.NET Core en Azure
App Service
25/06/2018 • 19 minutes to read • Edit Online

Por Luke Latham

IMPORTANT
Aviso sobre el uso de las versiones preliminares de ASP.NET Core 2.1
Consulte Implementar una versión preliminar de ASP.NET Core en Azure App Service.

En este artículo se proporcionan instrucciones sobre cómo diagnosticar un problema de inicio de aplicaciones
ASP.NET Core mediante herramientas de diagnóstico de Azure App Service. Puede encontrar consejos
adicionales de solución de problemas en Introducción a los diagnósticos de Azure App Service y Supervisión de
Aplicaciones en Azure App Service en la documentación de Azure.

Errores de inicio de aplicación


502.5 Error de proceso
El proceso de trabajo no funciona. La aplicación no se inicia.
El módulo ASP.NET Core intenta iniciar el proceso de trabajo, pero no lo consigue. Con frecuencia, examinar el
registro de eventos de la aplicación ayuda a solucionar problemas de este tipo. El acceso al registro se explica en
la sección Registro de eventos de la aplicación.
La página 502.5 Error de proceso se devuelve cuando una aplicación mal configurada provoca que el proceso de
trabajo genere un error:

500 Error interno del servidor


La aplicación se inicia, pero un error impide que el servidor complete la solicitud.
Este error se produce dentro del código de la aplicación durante el inicio o mientras se crea una respuesta. La
respuesta no puede contener nada o puede aparecer como 500 Error interno del servidor en el explorador. El
registro de eventos de la aplicación normalmente indica que la aplicación se ha iniciado normalmente. Desde la
perspectiva del servidor, eso es correcto. La aplicación se inició, pero no puede generar una respuesta válida.
Ejecute la aplicación en la consola de Kudu o habilite el registro de stdout del módulo ASP.NET Core para
solucionar el problema.
Restablecimiento de la conexión
Si se produce un error después de que se envían los encabezados, el servidor no tiene tiempo para enviar un
mensaje 500 Error interno del servidor cuando se produce un error. Esto suele ocurrir cuando se produce un
error durante la serialización de objetos complejos en una respuesta. Este tipo de error aparece como un error de
restablecimiento de la conexión en el cliente. El Registro de aplicaciones puede ayudar a solucionar estos tipos de
errores.

Límites de inicio predeterminados


El módulo ASP.NET Core está configurado con un valor predeterminado startupTimeLimit de 120 segundos.
Cuando se deja en el valor predeterminado, una aplicación puede tardar hasta dos minutos en iniciarse antes de
que el módulo registre un error de proceso. Para información sobre la configuración del módulo, consulte
Atributos del elemento aspNetCore.

Solución de problemas de errores de inicio de la aplicación


Registro de eventos de aplicación
Para acceder al registro de eventos de la aplicación, use la hoja Diagnose and solve problems (Diagnosticar y
resolver problemas) de Azure Portal:
1. En Azure Portal, abra la hoja de la aplicación en la hoja App Services.
2. Seleccione la hoja Diagnose and solve problems (Diagnosticar y resolver problemas).
3. En Seleccione una categoría de problema, seleccione el botón abajo Aplicación web.
4. En Suggested Solutions (Soluciones sugeridas), abra el panel de Open Application Event Logs (Abrir
registros de eventos de la aplicación). Seleccione el botón Open Application Event Logs (Abrir registros de
eventos de la aplicación).
5. Examine el error más reciente proporcionado por IIS AspNetCoreModule en la columna Origen.
Una alternativa al uso de la hoja Diagnose and solve problems (Diagnosticar y resolver problemas) es
examinar el archivo de registro de eventos de la aplicación directamente mediante Kudu:
1. Seleccione la hoja Herramientas avanzadas en el área Herramientas de desarrollo. Seleccione el botón
Ir→. Se abre la consola de Kudu en una nueva pestaña o ventana del explorador.
2. Mediante la barra de navegación de la parte superior de la página, abra la consola de depuración y
seleccione CMD.
3. Abra la carpeta LogFiles.
4. Seleccione el icono de lápiz junto al archivo eventlog.xml.
5. Examine el registro. Desplácese al final del registro para ver los eventos más recientes.
Ejecución de la aplicación en la consola de Kudu
Muchos errores de inicio no generan información útil en el registro de eventos de la aplicación. Puede ejecutar la
aplicación en la consola de ejecución remota de Kudu para detectar el error:
1. Seleccione la hoja Herramientas avanzadas en el área Herramientas de desarrollo. Seleccione el botón
Ir→. Se abre la consola de Kudu en una nueva pestaña o ventana del explorador.
2. Mediante la barra de navegación de la parte superior de la página, abra la consola de depuración y
seleccione CMD.
3. Abra las carpetas para la ruta de acceso site > wwwroot.
4. En la consola, ejecute la aplicación mediante la ejecución del ensamblado de la aplicación.
Si la aplicación es una implementación dependiente del marco, ejecute el ensamblado de la aplicación
con dotnet.exe. En el siguiente comando, sustituya el nombre del ensamblado de la aplicación por
<assembly_name> : dotnet .\<assembly_name>.dll
Si la aplicación es una implementación independiente, ejecute el archivo ejecutable de la aplicación. En
el siguiente comando, sustituya el nombre del ensamblado de la aplicación por <assembly_name> :
<assembly_name>.exe
5. La salida de consola de la aplicación, que muestra los posibles errores, se canaliza a la consola de Kudu.
Registro de stdout del módulo ASP.NET Core
El registro stdout del módulo ASP.NET Core con frecuencia registra mensajes de error útiles que no se
encuentran en el registro de eventos de la aplicación. Para habilitar y ver los registros de stdout:
1. Vaya a la hoja Diagnose and solve problems (Diagnosticar y resolver problemas) de Azure Portal.
2. En Seleccione una categoría de problema, seleccione el botón abajo Aplicación web.
3. En Suggested Solutions > (Soluciones sugeridas) Enable Stdout Log Redirection (Habilitar el
redireccionamiento de registros stdout), seleccione el botón para abrir la consola de Kudu y editar
web.config.
4. En la consola de diagnóstico de Kudu, abra las carpetas para la ruta de acceso site > wwwroot. Desplácese
hacia abajo para mostrar el archivo web.config en la parte inferior de la lista.
5. Haga clic en el icono de lápiz junto al archivo web.config.
6. Establezca stdoutLogEnabled en true y cambie la ruta de acceso de stdoutLogFile a:
\\?\%home%\LogFiles\stdout .
7. Seleccione Save (Guardar) para guardar el archivo web.config actualizado.
8. Realice una solicitud a la aplicación.
9. Vuelva a Azure Portal. Seleccione la hoja Herramientas avanzadas en el área Herramientas de desarrollo.
Seleccione el botón Ir→. Se abre la consola de Kudu en una nueva pestaña o ventana del explorador.
10. Mediante la barra de navegación de la parte superior de la página, abra la consola de depuración y
seleccione CMD.
11. Seleccione la carpeta LogFiles.
12. Inspeccione la columna Modificado y seleccione el icono de lápiz para editar el registro de stdout con la
última fecha de modificación.
13. Cuando se abre el archivo de registro, se muestra el error.
¡Importante! Deshabilite el registro de stdout cuando la solución de problemas haya finalizado.

1. En la consola de diagnóstico de Kudu, vuelva a la ruta de acceso site > wwwroot para mostrar el archivo
web.config. Seleccione el icono de lápiz para abrir de nuevo el archivo web.config.
2. Establezca stdoutLogEnabled en false .
3. Seleccione Save (Guardar) para guardar el archivo.

WARNING
La imposibilidad de deshabilitar el registro de stdout puede dar lugar a un error de la aplicación o del servidor. No hay
ningún límite en el tamaño del archivo de registro ni en el número de archivos de registro creados. Use únicamente el
registro de stdout para solucionar problemas de inicio de la aplicación.
Para el registro general en una aplicación ASP.NET Core, use una biblioteca de registro que limite el tamaño del archivo de
registro y realice la rotación de los registros. Para más información, consulte los proveedores de registro de terceros.

Errores comunes de inicio


Consulte la referencia de errores comunes de ASP.NET Core. La mayoría de los problemas comunes que impiden
el inicio de la aplicación se tratan en el tema de referencia.

Aplicación lenta o bloqueada


Cuando una aplicación responda con lentitud o se bloquee en una solicitud, consulte Solucionar los problemas de
rendimiento reducido de aplicaciones web en Azure App Service para obtener instrucciones de depuración.

Depuración remota
Consulte los temas siguientes:
Sección sobre la depuración remota de las aplicaciones web del artículo Solución de problemas de una
aplicación web en Azure App Service con Visual Studio (documentación de Azure)
Depuración remota de ASP.NET Core en IIS en Azure para Visual Studio 2017 (documentación de Visual
Studio)

Application Insights
Application Insights proporciona telemetría de las aplicaciones hospedadas en Azure App Service, lo que incluye
las características de registro de errores y generación de informes. Application Insights solo puede notificar los
errores que se producen después de que la aplicación se inicia cuando las características de registro de la
aplicación se vuelven disponibles. Para más información, consulte Application Insights para ASP.NET Core.

Hojas de supervisión
Las hojas de supervisión proporcionan una alternativa a la experiencia de solución de problemas de los métodos
descritos anteriormente en el tema. Estas hojas se pueden usar para diagnosticar errores de la serie 500.
Confirme que están instaladas las extensiones de ASP.NET Core. Si no lo están, instálelas manualmente:
1. En la sección de la hoja HERRAMIENTAS DE DESARROLLO, seleccione la hoja Extensiones.
2. Aparecerán en la lista las extensiones de ASP.NET Core.
3. Si las extensiones no están instaladas, seleccione el botón Add (Agregar).
4. Elija las extensiones de ASP.NET Core de la lista.
5. Seleccione Aceptar para aceptar los términos legales.
6. Seleccione Aceptar en la hoja Agregar extensión.
7. Un mensaje emergente informativo indica si las extensiones se han instalado correctamente.
Si el registro de stdout no está habilitado, siga estos pasos:
1. En Azure Portal, seleccione la hoja Herramientas avanzadas en el área HERRAMIENTAS DE
DESARROLLO. Seleccione el botón Ir→. Se abre la consola de Kudu en una nueva pestaña o ventana del
explorador.
2. Mediante la barra de navegación de la parte superior de la página, abra la consola de depuración y
seleccione CMD.
3. Abra las carpetas a la ruta de acceso sitio > wwwroot y desplácese hacia abajo para mostrar el archivo
web.config en la parte inferior de la lista.
4. Haga clic en el icono de lápiz junto al archivo web.config.
5. Establezca stdoutLogEnabled en true y cambie la ruta de acceso de stdoutLogFile a:
\\?\%home%\LogFiles\stdout .
6. Seleccione Save (Guardar) para guardar el archivo web.config actualizado.
Continúe para activar el registro de diagnóstico:
1. En Azure Portal, seleccione la hoja Registros de diagnóstico.
2. Seleccione el conmutador Activado en Registro de la aplicación (sistema de archivos) y Mensajes de
error detallados. Seleccione el botón Guardar en la parte superior de la hoja.
3. Para incluir el seguimiento de solicitudes con error, también conocido como almacenamiento en búfer de
eventos de solicitudes con error (FREB ), seleccione el conmutador Activado en Seguimiento de solicitudes
con error.
4. Seleccione la hoja Secuencia de registro, que aparece inmediatamente bajo la hoja Registros de
diagnóstico en el portal.
5. Realice una solicitud a la aplicación.
6. Dentro de los datos de la secuencia de registro, se indica la causa del error.
¡Importante! No olvide deshabilitar el registro de stdout cuando finalice la solución de problemas. Consulte las
instrucciones de la sección Registro de stdout del módulo ASP.NET Core.
Para ver los registros de seguimiento de solicitudes con error (registros FREB ):
1. Vaya a la hoja Diagnose and solve problems (Diagnosticar y resolver problemas) de Azure Portal.
2. Seleccione Failed Request Tracing Logs (Registros de seguimiento de solicitudes con error) en el área
SUPPORT TOOLS (HERRAMIENTAS DE SOPORTE TÉCNICO ) de la barra lateral.
Para más información, consulte la sección sobre los seguimientos de solicitudes con error del tema Habilitación
del registro de diagnóstico para aplicaciones web en Azure App Service y el artículo Preguntas más frecuentes
sobre el rendimiento de aplicaciones para Web Apps de Azure: ¿Cómo se activa el seguimiento de solicitudes con
error?.
Para más información, consulte Habilitación del registro de diagnóstico para aplicaciones web en Azure App
Service.

WARNING
La imposibilidad de deshabilitar el registro de stdout puede dar lugar a un error de la aplicación o del servidor. No hay
ningún límite en el tamaño del archivo de registro ni en el número de archivos de registro creados.
Para el registro rutinario en una aplicación ASP.NET Core, use una biblioteca de registro que limite el tamaño del archivo de
registro y realice la rotación de los registros. Para más información, consulte los proveedores de registro de terceros.

Recursos adicionales
Introducción a control de errores en ASP.NET Core
Referencia de errores comunes de Azure App Service e IIS con ASP.NET Core
Solución de problemas de una aplicación web en Azure App Service con Visual Studio
Solucionar los errores HTTP de "502 Puerta de enlace no válida" y "503 Servicio no disponible" en las
aplicaciones web de Azure
Solucionar los problemas de rendimiento reducido de aplicaciones web en Azure App Service
Preguntas más frecuentes sobre el rendimiento de aplicaciones para Web Apps de Azure
Azure Web App sandbox (App Service runtime execution limitations) (Espacio aislado de Azure Web App
[limitaciones de ejecución del entono de tiempo de ejecución de App Service])
Azure Friday: experiencia de diagnóstico y solución de problemas de Azure App Service (vídeo de 12 minutos)
Hospedaje de ASP.NET Core en Windows con IIS
28/05/2018 • 38 minutes to read • Edit Online

Por Luke Latham y Rick Anderson

Sistemas operativos admitidos


Los siguientes sistemas operativos son compatibles:
Windows 7 o posterior
Windows Server 2008 R2 o posterior
El servidor HTTP.sys (anteriormente denominado WebListener) no funciona en una configuración de proxy
inverso con IIS. Use el servidor Kestrel.

Configuración de aplicación
Habilitación de los componentes de integración con IIS
ASP.NET Core 2.x
ASP.NET Core 1.x
Los archivos Program.cs estándar llaman a CreateDefaultBuilder para empezar a configurar un host.
CreateDefaultBuilder configura Kestrel como el servidor web y habilita IIS Integration configurando la ruta de
acceso base y el puerto para el módulo ASP.NET Core:

public static IWebHost BuildWebHost(string[] args) =>


WebHost.CreateDefaultBuilder(args)
...

El módulo ASP.NET Core genera un puerto dinámico que se asigna al proceso back-end. El método
UseIISIntegration toma el puerto dinámico y configura Kestrel para que escuche en
http://localhost:{dynamicPort}/ . Esto invalida otras configuraciones de URL, como las llamadas a UseUrls o a
la API Listen de Kestrel. Por lo tanto, no es necesario realizar llamadas a UseUrls o a la API Listen de Kestrel
cuando se usa el módulo. Si se llama a UseUrls o Listen , Kestrel escucha en el puerto especificado cuando se
ejecuta la aplicación sin IIS.
Para obtener más información sobre el hospedaje, consulte Hospedaje en ASP.NET Core.
Opciones de IIS
Para configurar las opciones de IIS, incluya una configuración del servicio para IISOptions en
ConfigureServices. En el ejemplo siguiente, el reenvío de los certificados de cliente a la aplicación para rellenar
HttpContext.Connection.ClientCertificate está deshabilitado:

services.Configure<IISOptions>(options =>
{
options.ForwardClientCertificate = false;
});
OPCIÓN DEFAULT PARÁMETRO

AutomaticAuthentication true Si es true , el middleware de


integración con IIS establece el
HttpContext.User autenticado
mediante autenticación de Windows. Si
es false , el middleware solo
proporciona una identidad para
HttpContext.User y responde a los
desafíos cuando se le solicita
explícitamente mediante el
AuthenticationScheme .
Autenticación de Windows debe estar
habilitado en IIS para que
AutomaticAuthentication funcione.
Para más información, consulte el tema
Autenticación de Windows.

AuthenticationDisplayName null Establece el nombre para mostrar que


se muestra a los usuarios en las
páginas de inicio de sesión.

ForwardClientCertificate true Si
HttpContext.Connection.ClientCertificate
y el encabezado de solicitud true
está presente, se rellena
MS-ASPNETCORE-CLIENTCERT .

Escenarios de servidor proxy y equilibrador de carga


El software intermedio de integración con IIS, que configura el software intermedio de encabezados reenviados,
y el módulo de ASP.NET Core están configurados para reenviar el esquema (HTTP/HTTPS ) y la dirección IP
remota donde se originó la solicitud. Podría ser necesario realizar una configuración adicional para las
aplicaciones hospedadas detrás de servidores proxy y equilibradores de carga adicionales. Para más
información, vea Configurar ASP.NET Core para trabajar con servidores proxy y equilibradores de carga.
Archivo web.config
El archivo web.config configura el módulo ASP.NET Core. La creación, transformación y publicación de
web.config se controlan con el SDK web de .NET Core ( Microsoft.NET.Sdk.Web ). El SDK se establece en la parte
superior del archivo del proyecto:

<Project Sdk="Microsoft.NET.Sdk.Web">

Si el proyecto no incluye un archivo web.config, el archivo se crea con los elementos processPath y arguments
correctos para configurar el módulo ASP.NET Core y se mueve a la salida publicada.
Si el proyecto incluye un archivo web.config, el archivo se transforma con los elementos processPath y
arguments correctos para configurar el módulo ASP.NET Core y se mueve a la salida publicada. La
transformación no modifica los valores de configuración de IIS del archivo.
El archivo web.config puede proporcionar valores de configuración de IIS adicionales que controlan los
módulos activos de IIS. Para información sobre los módulos de IIS que son capaces de procesar las solicitudes
con aplicaciones ASP.NET Core, vea el tema Módulos IIS.
Para evitar que el SDK web transforme el archivo web.config, use la propiedad
<IsTransformWebConfigDisabled> en el archivo del proyecto:
<PropertyGroup>
<IsTransformWebConfigDisabled>true</IsTransformWebConfigDisabled>
</PropertyGroup>

Al deshabilitar el SDK web para la transformación del archivo, el desarrollador debe establecer el elemento
processPath y los argumentos manualmente. Para más información, vea ASP.NET Core Module configuration
reference (Referencia de configuración del módulo de ASP.NET Core).
Ubicación del archivo web.config
Para crear el proxy inverso entre IIS y el servidor de Kestrel, el archivo web.config debe estar presente en la ruta
de acceso raíz del contenido (normalmente la ruta de acceso base de la aplicación) de la aplicación
implementada. Se trata de la misma ubicación que la ruta de acceso física del sitio web proporcionada a IIS. El
archivo web.config debe estar en la raíz de la aplicación para habilitar la publicación de varias aplicaciones
mediante Web Deploy.
Los archivos confidenciales están en la ruta de acceso física de la aplicación, como
<ensamblado>.runtimeconfig.json, <ensamblado>.xml (comentarios de documentación XML ) y
<ensamblado>.deps.json. Cuando el archivo web.config está presente y el sitio se inicia normalmente, IIS no
sirve estos archivos confidenciales si se solicitan. Si el archivo web.config no está presente, se le asignó un
nombre incorrecto o no se puede configurar el sitio para un inicio normal, IIS puede servir archivos
confidenciales públicamente.
El archivo web.config debe estar presente en la implementación en todo momento, se le debe asignar
un nombre correcto y debe ser capaz de configurar el sitio para el inicio normal. Nunca quite el
archivo web.config de una implementación de producción.

Configuración de IIS
Sistemas operativos de servidor Windows
Habilite el rol de servidor Servidor web (IIS ) y establezca los servicios de rol.
1. Use el asistente Agregar roles y características del menú Administrar o el vínculo de Administrador
del servidor. En el paso Roles de servidor, active la casilla de Servidor web (IIS ).
2. Después del paso Características, el paso Servicios de rol se carga para el servidor Web (IIS ).
Seleccione los servicios de rol IIS que quiera o acepte los servicios de rol predeterminados
proporcionados.

Autenticación de Windows (opcional)


Para habilitar la autenticación de Windows, expanda los nodos siguientes: Servidor web > Seguridad.
Seleccione la característica Autenticación de Windows. Para más información, consulte Windows
Authentication <windowsAuthentication > (Autenticación de Windows ) y Configure Windows
authentication (Configurar la autenticación de Windows).
WebSockets (opcional)
WebSockets es compatible con ASP.NET Core 1.1 o posterior. Para habilitar WebSockets, expanda los
nodos siguientes: Servidor web > Desarrollo de aplicaciones. Seleccione la característica Protocolo
WebSocket. Para más información, vea WebSockets.
3. Continúe con el paso Confirmación para instalar el rol y los servicios de servidor web. No es necesario
reiniciar el servidor ni IIS después de instalar el rol Servidor web (IIS ).
Sistemas operativos de escritorio Windows
Habilite Consola de administración de IIS y Servicios World Wide Web.
1. Vaya a Panel de control > Programas > Programas y características > Activar o desactivar las
características de Windows (lado izquierdo de la pantalla).
2. Abra el nodo Internet Information Services. Abra el nodo Herramientas de administración web.
3. Active la casilla de Consola de administración de IIS.
4. Active la casilla de Servicios World Wide Web.
5. Acepte las características predeterminadas de Servicios World Wide Web o personalice las
características de IIS.
Autenticación de Windows (opcional)
Para habilitar la autenticación de Windows, expanda los nodos siguientes: Servicios World Wide Web
> Seguridad. Seleccione la característica Autenticación de Windows. Para más información, consulte
Windows Authentication <windowsAuthentication > (Autenticación de Windows ) y Configure Windows
authentication (Configurar la autenticación de Windows).
WebSockets (opcional)
WebSockets es compatible con ASP.NET Core 1.1 o posterior. Para habilitar WebSockets, expanda los
nodos siguientes: Servicios World Wide Web > Características de desarrollo de aplicaciones.
Seleccione la característica Protocolo WebSocket. Para más información, vea WebSockets.
6. Si la instalación de IIS requiere un reinicio, reinicie el sistema.
Instalación del conjunto de hospedaje de .NET Core
1. Instale el conjunto de hospedaje de .NET Core en el sistema de hospedaje. El lote instala .NET Core
Runtime, .NET Core Library y el módulo ASP.NET Core. El módulo crea el proxy inverso entre IIS y el
servidor Kestrel. Si el sistema no tiene conexión a Internet, obtenga e instale Microsoft Visual C++ 2015
Redistributable antes de instalar el conjunto de hospedaje de .NET Core.
a. Vaya a la página de todas las descargas de .NET.
b. Seleccione el tiempo de ejecución de .NET Core que no sea versión preliminar más reciente de la lista
(.NET Core > Tiempo de ejecución > Tiempo de ejecución de .NET Core x.y.z). A menos que
vaya a trabajar con software de versión preliminar, evite un entorno de tiempo de ejecución con la
palabra "vista previa" o "rc" (Release Candidate) en el texto del vínculo.
c. En la página de descarga de entornos de ejecución de .NET Core, en Windows, haga clic en el vínculo
del instalador del conjunto de hospedaje para descargar el conjunto de hospedaje de .NET Core.
¡Importante! Si el conjunto de hospedaje se instala antes que IIS, se debe reparar la instalación de
dicho conjunto. Vuelva a ejecutar el instalador del conjunto de hospedaje después de instalar IIS.
Para evitar que el instalador instale paquetes x86 en un sistema operativo x64, ejecute el instalador
desde un símbolo del sistema de administrador con el modificador OPT_NO_X86=1 .
2. Reinicie el sistema o ejecute net stop was /y seguido de net start w3svc desde un símbolo del sistema.
Al reiniciar IIS se recoge un cambio en la variable PATH del sistema realizado por el programa de
instalación.

NOTE
Para obtener información sobre la configuración compartida de IIS, vea ASP.NET Core Module with IIS Shared
Configuration (Módulo de ASP.NET Core con configuración compartida de IIS).

Instalación de Web Deploy al publicar con Visual Studio


Al implementar aplicaciones en servidores con Web Deploy, instale la versión más reciente de Web Deploy en
el servidor. Para instalar Web Deploy, use el Instalador de plataforma web (WebPI) u obtenga un instalador
directamente desde el Centro de descarga de Microsoft. El método preferido es usar WebPI. WebPI ofrece una
instalación independiente y una configuración para los proveedores de hospedaje.

Creación del sitio de IIS


1. En el sistema de hospedaje, cree una carpeta para que contenga los archivos y las carpetas publicados de
la aplicación. En el tema Estructura de directorios se describe el diseño de implementación de una
aplicación.
2. Dentro de la nueva carpeta, cree una carpeta logs para hospedar los registros de stdout del módulo
ASP.NET Core cuando esté habilitado el registro de stdout. Si la aplicación se ha implementado con una
carpeta logs en la carga, omita este paso. Para instrucciones sobre cómo habilitar MSBuild para crear la
carpeta logs automáticamente cuando el proyecto se crea de forma local, consulte el tema Estructura de
directorios.
IMPORTANT
Use solamente el registro de stdout para solucionar errores de inicio de aplicación. Nunca use el registro de
stdout para el registro de aplicaciones rutinarias. No hay ningún límite en el tamaño del archivo de registro ni en
el número de archivos de registro creados. El grupo de aplicaciones debe tener acceso de escritura a la ubicación
en la que se escriben los registros. Todas las carpetas de la ruta de acceso a la ubicación del registro deben existir.
Para más información sobre el registro de stdout, consulte Creación y redirección de registros. Para información
sobre el registro en una aplicación ASP.NET Core, vea el tema Registro.

3. En Administrador de IIS, abra el nodo del servidor en el panel Conexiones. Haga clic con el botón
derecho en la carpeta Sitios. Haga clic en Agregar sitio web en el menú contextual.
4. Proporcione el Nombre del sitio y establezca la Ruta de acceso física a la carpeta de implementación
de la aplicación. Proporcione la configuración de Enlace y cree el sitio web seleccionando Aceptar:

WARNING
Los enlaces de carácter comodín de nivel superior ( http://*:80/ y http://+:80 ) no se deben usar. Los
enlaces de carácter comodín de nivel superior pueden exponer su aplicación a vulnerabilidades de seguridad. Esto
se aplica tanto a los caracteres comodín fuertes como a los débiles. Use nombres de host explícitos en lugar de
caracteres comodín. Los enlaces de carácter comodín de subdominio (por ejemplo, *.mysub.com ) no suponen
este riesgo de seguridad si se controla todo el dominio primario (a diferencia de *.com , que sí es vulnerable). Vea
la sección 5.4 de RFC 7230 para obtener más información.

5. En el nodo del servidor, seleccione Grupos de aplicaciones.


6. Haga clic con el botón derecho en el grupo de aplicaciones del sitio y seleccione Configuración básica
en el menú contextual.
7. En la ventana Modificar grupo de aplicaciones, establezca Versión de .NET CLR en Sin código
administrado:

ASP.NET Core se ejecuta en un proceso independiente y administra el runtime. ASP.NET Core no se basa
en la carga de CLR de escritorio. El establecimiento de Versión de .NET CLR en Sin código
administrado es opcional.
8. Confirme que la identidad del modelo de proceso tiene los permisos adecuados.
Si cambia la identidad predeterminada del grupo de aplicaciones (Modelo de proceso > Identidad) de
ApplicationPoolIdentity a otra identidad, compruebe que la nueva identidad tenga los permisos
necesarios para obtener acceso a la carpeta de la aplicación, la base de datos y otros recursos necesarios.
Por ejemplo, el grupo de aplicaciones requiere acceso de lectura y escritura a las carpetas donde la
aplicación lee y escribe archivos.
Configuración de la autenticación de Windows (opcional)
Para más información, consulte Configurar la autenticación de Windows.

Implementación de la aplicación
Implemente la aplicación en la carpeta que ha creado en el sistema de hospedaje. Web Deploy es el mecanismo
recomendado para la implementación.
Web Deploy con Visual Studio
Vea el tema Visual Studio publish profiles for ASP.NET Core app deployment (Perfiles de publicación de Visual
Studio para la implementación de aplicaciones de ASP.NET Core) para obtener más información sobre cómo
crear un perfil de publicación para su uso con Web Deploy. Si el proveedor de hospedaje proporciona un perfil
de publicación o admite la creación de uno, descargue su perfil e impórtelo mediante el cuadro de diálogo
Publicar de Visual Studio.
Web Deploy fuera de Visual Studio
También puede usar Web Deploy fuera de Visual Studio desde la línea de comandos. Para más información, vea
Web Deployment Tool (Herramienta de implementación web).
Alternativas a Web Deploy
Use cualquiera de los métodos disponibles para mover la aplicación al sistema de hospedaje, como copia
manual, Xcopy, Robocopy o PowerShell.
Para obtener más información sobre ASP.NET Core en IIS, vea la sección Recursos de implementación para
administradores de IIS.

Examinar el sitio web

Archivos de implementación bloqueados


Los archivos de la carpeta de implementación se bloquean cuando se ejecuta la aplicación. Los archivos
bloqueados no se pueden sobrescribir durante la implementación. Para liberar archivos bloqueados de una
implementación, detenga el grupo de aplicaciones mediante uno de los enfoques siguientes:
Use Web Deploy con una referencia a Microsoft.NET.Sdk.Web en el archivo de proyecto. Se coloca un
archivo app_offline.htm en la raíz del directorio de aplicación web. Cuando el archivo está presente, el
módulo de ASP.NET Core cierra correctamente la aplicación y proporciona el archivo app_offline.htm
durante la implementación. Para más información, vea ASP.NET Core Module configuration reference
(Referencia de configuración del módulo de ASP.NET Core).
Detenga manualmente el grupo de aplicaciones en el Administrador de IIS en el servidor.
Use PowerShell para detener y reiniciar el grupo de aplicaciones (requiere PowerShell 5 o posterior):

$webAppPoolName = 'APP_POOL_NAME'

# Stop the AppPool


if((Get-WebAppPoolState $webAppPoolName).Value -ne 'Stopped') {
Stop-WebAppPool -Name $webAppPoolName
while((Get-WebAppPoolState $webAppPoolName).Value -ne 'Stopped') {
Start-Sleep -s 1
}
Write-Host `-AppPool Stopped
}

# Provide script commands here to deploy the app

# Restart the AppPool


if((Get-WebAppPoolState $webAppPoolName).Value -ne 'Started') {
Start-WebAppPool -Name $webAppPoolName
while((Get-WebAppPoolState $webAppPoolName).Value -ne 'Started') {
Start-Sleep -s 1
}
Write-Host `-AppPool Started
}

Protección de datos
La pila de protección de datos de ASP.NET Core la usan varios middlewares de ASP.NET Core, incluidos los que
se emplean en la autenticación. Aunque el código de usuario no llame a las API de protección de datos, la
protección de datos se debe configurar con un script de implementación o en el código de usuario para crear un
almacén de claves criptográficas persistente. Si no se configura la protección de datos, las claves se conservan
en memoria y se descartan cuando se reinicia la aplicación.
Si el conjunto de claves se almacena en memoria cuando se reinicia la aplicación:
Todos los tokens de autenticación basados en cookies se invalidan.
Los usuarios tienen que iniciar sesión de nuevo en la siguiente solicitud.
Ya no se pueden descifrar los datos protegidos con el conjunto de claves. Esto puede incluir tokens CSRF y
cookies de TempData de ASP.NET Core MVC.
Para configurar la protección de datos en IIS para conservar el conjunto de claves, use uno de los enfoques
siguientes:
Crear claves del Registro de protección de datos
Las claves de protección de datos que las aplicaciones de ASP.NET usan se almacenan en el Registro
externo a las aplicaciones. Para conservar las claves de una determinada aplicación, cree claves del
Registro para el grupo de aplicaciones.
En las instalaciones independientes de IIS que no son de granja de servidores web, puede usar el script
de PowerShell Provision-AutoGenKeys.ps1 de protección de datos para cada grupo de aplicaciones
usado con una aplicación de ASP.NET Core. Este script crea una clave del Registro en el registro HKLM
que solo es accesible a la cuenta de proceso de trabajo del grupo de aplicaciones de la aplicación. Las
claves se cifran en reposo mediante DPAPI con una clave de equipo.
En escenarios de granja de servidores web, una aplicación puede configurarse para usar una ruta de
acceso UNC para almacenar su conjunto de claves de protección de datos. De forma predeterminada, las
claves de protección de datos no se cifran. Asegúrese de que los permisos de archivo de un recurso
compartido de red se limitan a la cuenta de Windows bajo la que se ejecuta la aplicación. Puede usar un
certificado X509 para proteger las claves en reposo. Considere un mecanismo que permita a los usuarios
cargar certificados: coloque los certificados en el almacén de certificados de confianza del usuario y
asegúrese de que están disponibles en todos los equipos en los que se ejecuta la aplicación del usuario.
Vea Configurar la protección de datos en ASP.NET Core para más información.
Configurar el grupo de aplicaciones de IIS para cargar el perfil de usuario
Esta opción está en la sección Modelo de proceso, en la Configuración avanzada del grupo de
aplicaciones. Establezca Cargar perfil de usuario en True . Esto almacena las claves en el directorio del
perfil de usuario y las protege mediante DPAPI con una clave específica de la cuenta de usuario que el
grupo de aplicaciones usa.
Usar el sistema de archivos como un almacén de conjunto de claves
Ajuste el código de la aplicación para usar el sistema de archivos como un almacén de conjunto de
claves. Use un certificado X509 para proteger el conjunto de claves y asegúrese de que sea un certificado
de confianza. Si es un certificado autofirmado, colóquelo en el almacén raíz de confianza.
Cuando se usa IIS en una granja de servidores web:
Use un recurso compartido de archivos al que puedan acceder todos los equipos.
Implemente un certificado X509 en cada equipo. Configure la protección de datos en el código.
Establecer una directiva para todo el equipo para la protección de datos
El sistema de protección de datos tiene compatibilidad limitada con el establecimiento de una directiva
de equipo predeterminada para todas las aplicaciones que usan las API de protección de datos. Vea la
documentación de protección de datos para más detalles.

Configuración de aplicaciones secundarias


Las aplicaciones secundarias agregadas bajo la aplicación raíz no deben incluir el módulo de ASP.NET Core
como controlador. Si se agrega el módulo como controlador al archivo web.config de una aplicación secundaria,
aparece un error 500.19 (Error interno del servidor ) que hace referencia al archivo de configuración erróneo al
intentar examinar la aplicación secundaria.
En el ejemplo siguiente se muestra un archivo web.config publicado para una aplicación secundaria ASP.NET
Core:

<?xml version="1.0" encoding="utf-8"?>


<configuration>
<system.webServer>
<aspNetCore processPath="dotnet"
arguments=".\<assembly_name>.dll"
stdoutLogEnabled="false"
stdoutLogFile=".\logs\stdout" />
</system.webServer>
</configuration>
Al hospedar una aplicación secundaria que no es de ASP.NET Core bajo una aplicación de ASP.NET Core, quite
explícitamente el controlador heredado del archivo web.config de la aplicación secundaria:

<?xml version="1.0" encoding="utf-8"?>


<configuration>
<system.webServer>
<handlers>
<remove name="aspNetCore" />
</handlers>
<aspNetCore processPath="dotnet"
arguments=".\<assembly_name>.dll"
stdoutLogEnabled="false"
stdoutLogFile=".\logs\stdout" />
</system.webServer>
</configuration>

Para más información sobre la configuración del módulo de ASP.NET Core, vea el tema Introducción al módulo
de ASP.NET Core y ASP.NET Core Module configuration reference (Referencia de configuración del módulo de
ASP.NET Core).

Configuración de IIS con web.config


La configuración de IIS aún se ve afectada por la sección <system.webServer> de web.config en las
características de IIS que se aplican a una configuración de proxy inverso. Si IIS está configurado en el nivel de
servidor para usar compresión dinámica, el elemento <urlCompression> del archivo web.config de la
aplicación puede deshabilitarla.
Para más información, vea la referencia de configuración para <system.webServer>, Referencia de
configuración del módulo de ASP.NET Core y Módulos IIS con ASP.NET Core. Para establecer variables de
entorno para aplicaciones individuales que se ejecutan en grupos de aplicaciones aislados (se admite para IIS
10.0 o posterior), vea la sección AppCmd.exe command (Comando AppCmd.exe) del tema Environment
Variables <environmentVariables> (Variables de entorno ) de la documentación de referencia de IIS.

Secciones de configuración de web.config


Las aplicaciones ASP.NET Core no usan las secciones de configuración de aplicaciones ASP.NET 4.x en
web.config para la configuración:
<system.web>
<appSettings>
<connectionStrings>
<location>
Las aplicaciones de ASP.NET Core se configuran mediante otros proveedores de configuración. Para obtener
más información, vea Configuración.

Grupos de aplicaciones
Al hospedar varios sitios web en un servidor, aísle las aplicaciones entre sí mediante la ejecución de cada una de
ellas en su propio grupo de aplicaciones. El valor predeterminado del cuadro de diálogo Agregar sitio web es
esta configuración. Cuando se proporciona el Nombre del sitio, el texto se transfiere automáticamente al
cuadro de texto Grupo de aplicaciones. Al agregar el sitio se crea un grupo de aplicaciones con el nombre del
sitio.

Identidad del grupo de aplicaciones


Una cuenta de identidad del grupo de aplicaciones permite ejecutar una aplicación en una cuenta única sin
tener que crear ni administrar dominios o cuentas locales. En IIS 8.0 o versiones posteriores, el proceso de
trabajo de administración de IIS (WAS ) crea una cuenta virtual con el nombre del nuevo grupo de aplicaciones
y ejecuta los procesos de trabajo del grupo de aplicaciones en esta cuenta de forma predeterminada. En la
Consola de administración de IIS, en la opción Configuración avanzada del grupo de aplicaciones, asegúrese
de que la Identidad está establecida para usar ApplicationPoolIdentity:

El proceso de administración de IIS crea un identificador seguro con el nombre del grupo de aplicaciones en el
sistema de seguridad de Windows. Los recursos se pueden proteger mediante esta identidad. Sin embargo, no
es una cuenta de usuario real ni se muestra en la consola de administración de usuario de Windows.
Si el proceso de trabajo de IIS requiere acceso con privilegios elevados a la aplicación, modifique la lista de
control de acceso (ACL ) del directorio que contiene la aplicación:
1. Abra el Explorador de Windows y vaya al directorio.
2. Haga clic con el botón derecho en el directorio y seleccione Propiedades.
3. En la pestaña Seguridad, haga clic en el botón Editar y en el botón Agregar.
4. Haga clic en el botón Ubicaciones y asegúrese de seleccionar el sistema.
5. Escriba IIS AppPool\<nombre_del_grupo_de_aplicaciones> en el área Escribir los nombres de
objeto para seleccionar. Haga clic en el botón Comprobar nombres. Para DefaultAppPool
compruebe los nombres con IIS AppPool\DefaultAppPool. Cuando el botón Comprobar nombres
está seleccionado, un valor de DefaultAppPool se indica en el área de los nombres de objeto. No es
posible escribir el nombre del grupo de aplicaciones directamente en el área de los nombres de objeto.
Use el formato IIS AppPool\<nombre_del_grupo_de_aplicaciones> cuando compruebe el nombre
del objeto.
6. Seleccione Aceptar.

7. Los permisos de lectura y ejecución se deben conceder de forma predeterminada. Proporcione permisos
adicionales según sea necesario.
El acceso también se puede conceder mediante un símbolo del sistema con la herramienta ICACLS. En el
siguiente comando se usa DefaultAppPool como ejemplo:

ICACLS C:\sites\MyWebApp /grant "IIS AppPool\DefaultAppPool":F

Para más información, consulte el tema icacls.

Recursos de implementación para administradores de IIS


Obtenga información detallada sobre IIS en su documentación.
Documentación de IIS
Obtenga información sobre los modelos de implementación de aplicaciones .NET Core.
Implementación de aplicaciones .NET Core
Obtenga información sobre cómo el módulo de ASP.NET Core permite que el servidor web de Kestrel use IIS o
IIS Express como servidor proxy inverso.
Módulo ASP.NET Core
Obtenga información sobre cómo configurar el módulo de ASP.NET Core para hospedar aplicaciones ASP.NET
Core.
Referencia de configuración del módulo ASP.NET Core
Obtenga información sobre la estructura de directorios de las aplicaciones ASP.NET Core publicadas.
Estructura de directorios
Descubra módulos activos e inactivos de IIS para aplicaciones ASP.NET Core y cómo administrar módulos de
IIS.
Módulos de IIS
Obtenga información sobre cómo diagnosticar problemas con las implementaciones de aplicaciones ASP.NET
Core por parte de IIS.
Solucionar problemas
Identifique los errores comunes al hospedar aplicaciones ASP.NET Core en IIS.
Referencia de errores comunes de Azure App Service e IIS

Recursos adicionales
Introducción a ASP.NET Core
Sitio oficial de Microsoft IIS
Biblioteca de contenido técnico de Windows Server
Solución de problemas de ASP.NET Core en IIS
25/06/2018 • 15 minutes to read • Edit Online

Por Luke Latham


En este artículo se proporcionan instrucciones sobre cómo diagnosticar un problema de inicio de ASP.NET Core
al hospedarse con Internet Information Services (IIS ). La información de este artículo se aplica al hospedaje en
IIS en Windows Server y el escritorio de Windows.
En Visual Studio, un proyecto de ASP.NET Core toma como predeterminado el hospedaje de IIS Express durante
la depuración. El código de estado 502.5 Error de proceso que se produce al realizar la depuración localmente se
puede solucionar si se sigue el consejo de este tema.
Temas adicionales de solución de problemas:
Solución de problemas de ASP.NET Core en Azure App Service
Aunque App Service usa el módulo ASP.NET Core e IIS para hospedar las aplicaciones, consulte el tema
dedicado para obtener instrucciones específicas.
Control de errores
Descubra cómo controlar los errores de aplicaciones de ASP.NET Core durante el desarrollo en un sistema local.
Información sobre cómo depurar con Visual Studio
En este tema se presentan las características del depurador de Visual Studio.

Errores de inicio de aplicación


502.5 Error de proceso
El proceso de trabajo no funciona. La aplicación no se inicia.
El módulo ASP.NET Core intenta iniciar el proceso de trabajo, pero no lo consigue. La causa del error de inicio
del proceso se suele determinar a partir de las entradas del registro de eventos de la aplicación y del registro de
stdout del módulo ASP.NET Core.
La página de error 502.5 Error de proceso se devuelve cuando el proceso de trabajo no se puede iniciar debido a
un error de configuración de la aplicación o del hospedaje:

500 Error interno del servidor


La aplicación se inicia, pero un error impide que el servidor complete la solicitud.
Este error se produce dentro del código de la aplicación durante el inicio o mientras se crea una respuesta. La
respuesta no puede contener nada o puede aparecer como 500 Error interno del servidor en el explorador. El
registro de eventos de la aplicación normalmente indica que la aplicación se ha iniciado normalmente. Desde la
perspectiva del servidor, eso es correcto. La aplicación se inició, pero no puede generar una respuesta válida.
Ejecute la aplicación en un símbolo del sistema en el servidor o habilite el registro de stdout del módulo
ASP.NET Core para solucionar el problema.
Restablecimiento de la conexión
Si se produce un error después de que se envían los encabezados, el servidor no tiene tiempo para enviar un
mensaje 500 Error interno del servidor cuando se produce un error. Esto suele ocurrir cuando se produce un
error durante la serialización de objetos complejos en una respuesta. Este tipo de error aparece como un error
de restablecimiento de la conexión en el cliente. El Registro de aplicaciones puede ayudar a solucionar estos
tipos de errores.

Límites de inicio predeterminados


El módulo ASP.NET Core está configurado con un valor predeterminado startupTimeLimit de 120 segundos.
Cuando se deja en el valor predeterminado, una aplicación puede tardar hasta dos minutos en iniciarse antes de
que el módulo registre un error de proceso. Para información sobre la configuración del módulo, consulte
Atributos del elemento aspNetCore.

Solución de problemas de errores de inicio de la aplicación


Registro de eventos de aplicación
Acceda al registro de eventos de la aplicación:
1. Abra el menú Inicio, busque Visor de eventos y luego seleccione la aplicación Visor de eventos.
2. En Visor de eventos, abra el nodo Registros de Windows.
3. Seleccione Aplicación para abrir el registro de eventos de la aplicación.
4. Busque los errores asociados a la aplicación objeto del error. Los errores tienen un valor de Módulo
AspNetCore de IIS o Módulo AspNetCore de IIS Express en la columna Origen.
Ejecución de la aplicación en un símbolo del sistema
Muchos errores de inicio no generan información útil en el registro de eventos de la aplicación. La causa de
algunos errores se puede encontrar mediante la ejecución de la aplicación en un símbolo del sistema en el
sistema de hospedaje.
Implementación dependiente de marco
Si la aplicación es una implementación dependiente del marco:
1. En un símbolo del sistema, vaya a la carpeta de implementación y ejecute la aplicación mediante la ejecución
del ensamblado de la aplicación con dotnet.exe. En el siguiente comando, sustituya el nombre del
ensamblado de la aplicación por <nombre_de_ensamblado>: dotnet .\<assembly_name>.dll .
2. La salida de consola de la aplicación, que muestra los posibles errores, se escribe en la ventana de la consola.
3. Si los errores se producen al realizar una solicitud a la aplicación, realice una solicitud al host y el puerto
donde escucha Kestrel. Con el host y el puerto predeterminados, realizar una solicitud a
http://localhost:5000/ . Si la aplicación responde normalmente en la dirección del punto de conexión de
Kestrel, es más probable que el problema esté relacionado con la configuración del proxy inverso que con la
propia aplicación.
Implementación independiente
Si la aplicación es una implementación independiente:
1. En un símbolo del sistema, vaya a la carpeta de implementación y ejecute el archivo ejecutable de la
aplicación. En el siguiente comando, sustituya el nombre del ensamblado de la aplicación por
<nombre_de_ensamblado>: <assembly_name>.exe .
2. La salida de consola de la aplicación, que muestra los posibles errores, se escribe en la ventana de la consola.
3. Si los errores se producen al realizar una solicitud a la aplicación, realice una solicitud al host y el puerto
donde escucha Kestrel. Con el host y el puerto predeterminados, realizar una solicitud a
http://localhost:5000/ . Si la aplicación responde normalmente en la dirección del punto de conexión de
Kestrel, es más probable que el problema esté relacionado con la configuración del proxy inverso que con la
propia aplicación.
Registro de stdout del módulo ASP.NET Core
Para habilitar y ver los registros de stdout:
1. Vaya a la carpeta de implementación del sitio en el sistema de hospedaje.
2. Si la carpeta Logs no existe, cree la carpeta. Para obtener instrucciones sobre cómo habilitar MSBuild para
crear la carpeta logs automáticamente en la implementación, consulte el tema Estructura de directorios.
3. Edite el archivo web.config. Establezca stdoutLogEnabled en true y cambie la ruta de acceso de
stdoutLogFile para que apunte a la carpeta logs (por ejemplo, .\logs\stdout ). stdout en la ruta de acceso
es el prefijo del nombre del archivo de registro. Cuando se crea el registro, se agregan automáticamente una
marca de tiempo, un identificador de proceso y una extensión de archivo. Cuando se usa stdout como
prefijo para el nombre de archivo, un archivo de registro se llama normalmente
stdout_20180205184032_5412.log.
4. Guarde el archivo web.config actualizado.
5. Realice una solicitud a la aplicación.
6. Vaya a la carpeta logs. Busque y abra el registro más reciente de stdout.
7. Estudie el registro para ver los errores.
¡Importante! Deshabilite el registro de stdout cuando la solución de problemas haya finalizado.

1. Edite el archivo web.config.


2. Establezca stdoutLogEnabled en false .
3. Guarde el archivo.

WARNING
La imposibilidad de deshabilitar el registro de stdout puede dar lugar a un error de la aplicación o del servidor. No hay
ningún límite en el tamaño del archivo de registro ni en el número de archivos de registro creados.
Para el registro rutinario en una aplicación ASP.NET Core, use una biblioteca de registro que limite el tamaño del archivo de
registro y realice la rotación de los registros. Para más información, consulte los proveedores de registro de terceros.

Habilitación de la página de excepciones del desarrollador


La variable de entorno ASPNETCORE_ENVIRONMENT se puede agregar a web.config para ejecutar la aplicación en el
entorno de desarrollo. Siempre y cuando el entorno no se invalide al inicio de la aplicación con UseEnvironment
en el generador de host, la configuración de la variable de entorno permite que aparezca la página de
excepciones del desarrollador cuando se ejecuta la aplicación.
<aspNetCore processPath="dotnet"
arguments=".\MyApp.dll"
stdoutLogEnabled="false"
stdoutLogFile=".\logs\stdout">
<environmentVariables>
<environmentVariable name="ASPNETCORE_ENVIRONMENT" value="Development" />
</environmentVariables>
</aspNetCore>

Solo se recomienda establecer la variable de entorno para ASPNETCORE_ENVIRONMENT cuando se use en servidores
de ensayo o pruebas que no estén expuestos a Internet. Quite la variable de entorno del archivo web.config
cuando termine de solucionar los problemas. Para información sobre la configuración de variables de entorno
en web.config, consulte el elemento secundario environmentVariables de aspNetCore.

Errores comunes de inicio


Consulte la referencia de errores comunes de ASP.NET Core. La mayoría de los problemas comunes que
impiden el inicio de la aplicación se tratan en el tema de referencia.

Aplicación lenta o bloqueada


Cuando una aplicación responda con lentitud o queda bloqueado en una solicitud, obtenga y analice un archivo
de volcado de memoria. Los archivos de volcado de memoria se pueden obtener mediante cualquiera de las
siguientes herramientas:
ProcDump
DebugDiag
WinDbg: Download Debugging tools for Windows (Descarga de herramientas de depuración para
Windows), Debugging Using WinDbg (Depuración mediante WinDbg)

Depuración remota
Consulte Depuración remota de ASP.NET Core en un equipo remoto de IIS en Visual Studio 2017 en la
documentación de Visual Studio.

Application Insights
Application Insights proporciona telemetría de las aplicaciones hospedadas en IIS, lo que incluye las
características de registro de errores y generación de informes. Application Insights solo puede notificar los
errores que se producen después de que la aplicación se inicia cuando las características de registro de la
aplicación se vuelven disponibles. Para más información, consulte Application Insights para ASP.NET Core.

Consejos adicionales de solución de problemas


En ocasiones, una aplicación en funcionamiento deja de funcionar inmediatamente después de actualizar el SDK
de .NET Core en la máquina de desarrollo o las versiones del paquete en la aplicación. En algunos casos, los
paquetes incoherentes pueden interrumpir una aplicación al realizar actualizaciones importantes. La mayoría de
estos problemas puede corregirse siguiendo estas instrucciones:
1. Elimine las carpetas bin y obj.
2. Borre las memorias caché del paquete en %UserProfile%\.nuget\packages y %LocalAppData%\Nuget\v3 -
cache.
3. Restaure el proyecto y vuelva a compilarlo.
4. Confirme que la implementación anterior en el servidor se ha eliminado por completo antes de volver a
implementar la aplicación.

TIP
Una forma práctica de borrar las memorias cachés del paquete es ejecutar dotnet nuget locals all --clear desde un
símbolo del sistema.
Otra manera de borrar las memorias caché del paquete es usar la herramienta nuget.exe y ejecutar el comando
nuget locals all -clear . nuget.exe no es una instalación agrupada con el sistema operativo de escritorio de Windows
y se debe obtener de forma independiente en el sitio web de NuGet.

Recursos adicionales
Introducción a control de errores en ASP.NET Core
Referencia de errores comunes de Azure App Service e IIS con ASP.NET Core
Referencia de configuración del módulo ASP.NET Core
Solución de problemas de ASP.NET Core en Azure App Service
Referencia de configuración del módulo ASP.NET
Core
25/06/2018 • 22 minutes to read • Edit Online

Por Luke Latham, Rick Anderson y Sourabh Shirhatti


En este documento se proporcionan instrucciones sobre cómo configurar el módulo ASP.NET Core para
hospedar aplicaciones de ASP.NET Core. Puede encontrar una introducción al módulo ASP.NET Core e
instrucciones de instalación en el artículo de introducción al módulo ASP.NET Core.

Configuración con web.config


El módulo ASP.NET Core se configura con la sección aspNetCore del nodo system.webServer del archivo
web.config del sitio.
El siguiente archivo web.config se publica para una implementación dependiente del marco y configura el
módulo ASP.NET Core para controlar las solicitudes de sitios:

<?xml version="1.0" encoding="utf-8"?>


<configuration>
<system.webServer>
<handlers>
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified" />
</handlers>
<aspNetCore processPath="dotnet"
arguments=".\MyApp.dll"
stdoutLogEnabled="false"
stdoutLogFile=".\logs\stdout" />
</system.webServer>
</configuration>

El siguiente archivo web.config se publica para una implementación independiente:

<?xml version="1.0" encoding="utf-8"?>


<configuration>
<system.webServer>
<handlers>
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified" />
</handlers>
<aspNetCore processPath=".\MyApp.exe"
stdoutLogEnabled="false"
stdoutLogFile=".\logs\stdout" />
</system.webServer>
</configuration>

Cuando se implementa una aplicación en Azure App Service, la ruta de acceso de stdoutLogFile se establece
en \\?\%home%\LogFiles\stdout . La ruta de acceso guarda los registros de stdout en la carpeta LogFiles, que es
una ubicación que el servicio crea automáticamente.
Consulte Configuración de aplicaciones secundarias para ver una nota importante relativa a la configuración
de archivos web.config en aplicaciones secundarias.
Atributos del elemento aspNetCore
ATRIBUTO DESCRIPTION DEFAULT

arguments Atributo de cadena opcional.


Argumentos para el archivo
ejecutable especificado en
processPath.

disableStartUpErrorPage true o false false

Si es true, la página 502.5 - Error


en el proceso se suprime, y tiene
prioridad la página de código de
estado 502 configurada en
web.config.

forwardWindowsAuthToken true o false true

Si es true, el token se reenvía al


proceso secundario que escucha
en % ASPNETCORE_PORT % como
un encabezado "MS-
ASPNETCORE-WINAUTHTOKEN"
por solicitud. Es responsabilidad
de dicho proceso llamar a
CloseHandle en este token por
solicitud.

processPath Atributo de cadena necesario.


Ruta de acceso al archivo
ejecutable que inicia un proceso
que escucha las solicitudes HTTP.
No se admiten rutas de acceso
relativas. Si la ruta de acceso
comienza con . , se considera
que es relativa a la raíz del sitio.

rapidFailsPerMinute Atributo integer opcional. 10

Especifica el número de veces que


el proceso indicado en
processPath puede bloquearse
por minuto. Si se supera este
límite, el módulo deja de iniciar el
proceso durante lo que resta del
minuto.
ATRIBUTO DESCRIPTION DEFAULT

requestTimeout Atributo timespan opcional. 00:02:00

Especifica el tiempo que el módulo


ASP.NET Core espera una
respuesta del proceso que escucha
en % ASPNETCORE_PORT %.
En las versiones del módulo
ASP.NET Core que se envían con la
versión de ASP.NET Core 2.0 o
anterior, requestTimeout solo se
debe especificar en minutos
enteros, si no, adopta el valor
predeterminado de 2 minutos.

shutdownTimeLimit Atributo integer opcional. 10

Tiempo en segundos que el


módulo espera a que se cierre
correctamente el archivo
ejecutable cuando se detecta el
archivo app_offline.htm.

startupTimeLimit Atributo integer opcional. 120

Tiempo en segundos que espera el


módulo a que el archivo ejecutable
inicie u proceso que escucha en el
puerto. Si se supera este límite de
tiempo, el módulo termina el
proceso. El módulo intenta
reiniciar el proceso cuando se
recibe una nueva solicitud y lo
sigue intentando en las sucesivas
solicitudes entrantes a no ser que
la aplicación no pueda iniciar
rapidFailsPerMinute un número
de veces en el último minuto
acumulado.

stdoutLogEnabled Atributo Boolean opcional. false

Si es true, stdout y stderr en el


proceso especificado en
processPath se redirigen al
archivo especificado en
stdoutLogFile.
ATRIBUTO DESCRIPTION DEFAULT

stdoutLogFile Atributo de cadena opcional. aspnetcore-stdout

Especifica la ruta de acceso relativa


o absoluta para la que se registran
stdout y stderr desde el proceso
especificado en processPath. Las
rutas de acceso relativas son
relativas a la raíz del sitio.
Cualquier ruta de acceso que se
inicia con . es relativa a la raíz
del sitio y todas las demás rutas
de acceso se tratan como
absolutas. Las carpetas que se
proporcionan en la ruta de acceso
deben estar en orden para que el
módulo cree el archivo de registro.
Mediante delimitadores se agrega
una marca de tiempo, un
identificador de proceso y una
extensión de archivo (.log) al
último segmento de la ruta de
acceso stdoutLogFile. Si
.\logs\stdout se proporciona
como valor, se guarda un registro
de ejemplo de stdout como
stdout_20180205194132_1934.lo
g en la carpeta logs, cuando se
guarda el 5/2/2018 a las 19:41:32
con un identificador de proceso de
1934.

ATRIBUTO DESCRIPTION DEFAULT

arguments Atributo de cadena opcional.


Argumentos para el archivo
ejecutable especificado en
processPath.

disableStartUpErrorPage true o false false

Si es true, la página 502.5 - Error


en el proceso se suprime, y tiene
prioridad la página de código de
estado 502 configurada en
web.config.

forwardWindowsAuthToken true o false true

Si es true, el token se reenvía al


proceso secundario que escucha
en % ASPNETCORE_PORT % como
un encabezado "MS-
ASPNETCORE-WINAUTHTOKEN"
por solicitud. Es responsabilidad
de dicho proceso llamar a
CloseHandle en este token por
solicitud.
ATRIBUTO DESCRIPTION DEFAULT

processPath Atributo de cadena necesario.


Ruta de acceso al archivo
ejecutable que inicia un proceso
que escucha las solicitudes HTTP.
No se admiten rutas de acceso
relativas. Si la ruta de acceso
comienza con . , se considera
que es relativa a la raíz del sitio.

rapidFailsPerMinute Atributo integer opcional. 10

Especifica el número de veces que


el proceso indicado en
processPath puede bloquearse
por minuto. Si se supera este
límite, el módulo deja de iniciar el
proceso durante lo que resta del
minuto.

requestTimeout Atributo timespan opcional. 00:02:00

Especifica el tiempo que el módulo


ASP.NET Core espera una
respuesta del proceso que escucha
en % ASPNETCORE_PORT %.
En las versiones del módulo
ASP.NET Core que se envían con la
versión de ASP.NET Core 2.1 o
posterior, el valor
requestTimeout se especifica en
horas, minutos y segundos.

shutdownTimeLimit Atributo integer opcional. 10

Tiempo en segundos que el


módulo espera a que se cierre
correctamente el archivo
ejecutable cuando se detecta el
archivo app_offline.htm.

startupTimeLimit Atributo integer opcional. 120

Tiempo en segundos que espera el


módulo a que el archivo ejecutable
inicie u proceso que escucha en el
puerto. Si se supera este límite de
tiempo, el módulo termina el
proceso. El módulo intenta
reiniciar el proceso cuando se
recibe una nueva solicitud y lo
sigue intentando en las sucesivas
solicitudes entrantes a no ser que
la aplicación no pueda iniciar
rapidFailsPerMinute un número
de veces en el último minuto
acumulado.
ATRIBUTO DESCRIPTION DEFAULT

stdoutLogEnabled Atributo Boolean opcional. false

Si es true, stdout y stderr en el


proceso especificado en
processPath se redirigen al
archivo especificado en
stdoutLogFile.

stdoutLogFile Atributo de cadena opcional. aspnetcore-stdout

Especifica la ruta de acceso relativa


o absoluta para la que se registran
stdout y stderr desde el proceso
especificado en processPath. Las
rutas de acceso relativas son
relativas a la raíz del sitio.
Cualquier ruta de acceso que se
inicia con . es relativa a la raíz
del sitio y todas las demás rutas
de acceso se tratan como
absolutas. Las carpetas que se
proporcionan en la ruta de acceso
deben estar en orden para que el
módulo cree el archivo de registro.
Mediante delimitadores se agrega
una marca de tiempo, un
identificador de proceso y una
extensión de archivo (.log) al
último segmento de la ruta de
acceso stdoutLogFile. Si
.\logs\stdout se proporciona
como valor, se guarda un registro
de ejemplo de stdout como
stdout_20180205194132_1934.lo
g en la carpeta logs, cuando se
guarda el 5/2/2018 a las 19:41:32
con un identificador de proceso de
1934.

Configuración de las variables de entorno


Se pueden especificar variables de entorno para el proceso en el atributo processPath . Especifique una
variable de entorno con el elemento secundario environmentVariable de un elemento de la colección
environmentVariables . Las variables de entorno establecidas en esta sección tienen prioridad sobre las
variables del entorno del sistema.
En el ejemplo siguiente se establecen dos variables de entorno. ASPNETCORE_ENVIRONMENT configura el entorno
de la aplicación como Development . Un desarrollador puede establecer temporalmente este valor en el archivo
web.config con el fin de forzar a que se cargue la página de excepciones del desarrollador al depurar una
excepción de aplicación. CONFIG_DIR es un ejemplo de una variable de entorno definida por el usuario, donde el
desarrollador ha escrito código que lee el valor al inicio para formar una ruta de acceso destinada a la carga del
archivo de configuración de la aplicación.
<aspNetCore processPath="dotnet"
arguments=".\MyApp.dll"
stdoutLogEnabled="false"
stdoutLogFile="\\?\%home%\LogFiles\stdout">
<environmentVariables>
<environmentVariable name="ASPNETCORE_ENVIRONMENT" value="Development" />
<environmentVariable name="CONFIG_DIR" value="f:\application_config" />
</environmentVariables>
</aspNetCore>

WARNING
Establezca solo la variable de entorno ASPNETCORE_ENVIRONMENT en Development en servidores de ensayo y pruebas a
los que no puedan acceder redes que no son de confianza, como Internet.

app_offline.htm
Si un archivo con el nombre app_offline.htm se detecta en el directorio raíz de una aplicación, el módulo
ASP.NET Core intenta cerrar correctamente la aplicación y deja de procesar las solicitudes entrantes. Si la
aplicación se sigue ejecutando después del número definido en shutdownTimeLimit , el módulo ASP.NET Core
termina el proceso en ejecución.
Mientras el archivo app_offline.htm existe, el módulo ASP.NET Core responde a solicitudes con la devolución
del contenido del archivo app_offline.htm. Cuando se quita el archivo app_offline.htm, la solicitud siguiente
inicia la aplicación.

Página de errores de inicio


Si el módulo ASP.NET Core no es capaz de iniciar el proceso de back-end o este se inicia pero no puede
escuchar en el puerto configurado, aparece una página de código de estado 502.5 Error de proceso. Para
suprimir esta página y volver a la página de código de estado 502 de IIS predeterminada, use el atributo
disableStartUpErrorPage . Para más información sobre cómo configurar mensajes de error personalizados,
consulte Errores HTTP <httpErrors> .
Creación y redireccionamiento de registros
El módulo ASP.NET Core redirige los registros stdout y stderr al disco si se establecen los atributos
stdoutLogEnabled y stdoutLogFile del elemento aspNetCore . Las carpetas de la ruta de acceso stdoutLogFile
debe estar en orden para que el módulo cree el archivo de registro. El grupo de aplicaciones debe tener acceso
de escritura a la ubicación en la que se escriben los registros (use IIS AppPool\<app_pool_name> para
proporcionar permiso de escritura).
Los registros no se rotan, a no ser que se produzca un reinicio o reciclaje del proceso. Es responsabilidad del
proveedor de servicios de hospedaje limitar el espacio en disco que consumen los registros.
El uso del registro de stdout solo se recomienda para solucionar problemas de inicio de la aplicación. No use el
registro de stdout con fines de registro de aplicaciones general. Para el registro rutinario en una aplicación
ASP.NET Core, use una biblioteca de registro que limite el tamaño del archivo de registro y realice la rotación
de los registros. Para más información, consulte los proveedores de registro de terceros.
Cuando se crea el archivo de registro, se agregan automáticamente una marca de tiempo y una extensión de
archivo. El nombre del archivo de registro se forma mediante la anexión de la marca de tiempo, el identificador
de proceso y la extensión de archivo (.log) al último segmento de la ruta de acceso stdoutLogFile
(normalmente stdout) delimitados por caracteres de subrayado. Si la ruta de acceso de stdoutLogFile finaliza
con stdout, el registro de una aplicación con un PID de 1934 creado el 5/2/2018 a las 19:42:32 tiene el nombre
de archivo stdout_20180205194132_1934.log.
El elemento de ejemplo siguiente, aspNetCore , configura el registro de stdout para una aplicación hospedada
en Azure App Service. Una ruta de acceso local o una ruta de acceso de recurso compartido de red son
aceptables para el registro local. Confirme que la identidad del usuario de AppPool tenga permiso para escribir
en la ruta de acceso proporcionada.

<aspNetCore processPath="dotnet"
arguments=".\MyApp.dll"
stdoutLogEnabled="true"
stdoutLogFile="\\?\%home%\LogFiles\stdout">
</aspNetCore>

Consulte Configuración con web.config para ver un ejemplo del elemento aspNetCore en el archivo web.config.

La configuración de proxy usa el protocolo HTTP y un token de


emparejamiento
El proxy creado entre el módulo ASP.NET Core y Kestrel usa el protocolo HTTP. El uso de HTTP optimiza el
rendimiento, ya que el tráfico entre el módulo y Kestrel se realiza en una dirección de bucle invertido fuera de
la interfaz de red. No hay ningún riesgo de interceptación del tráfico entre el módulo y Kestrel desde una
ubicación fuera del servidor.
Un token de emparejamiento sirve para garantizar que las solicitudes recibidas por Kestrel se redirigieron
mediante proxy por IIS y no procedieron de otra fuente. El módulo crea el token de emparejamiento y lo
establece en una variable de entorno ( ASPNETCORE_TOKEN ). El token de emparejamiento también se establece en
un encabezado ( MSAspNetCoreToken ) en cada solicitud redirigida mediante proxy. El middleware de IIS
comprueba cada solicitud recibida para confirmar que el valor del encabezado del token de emparejamiento
coincida con el valor de la variable de entorno. Si los valores del token no coinciden, la solicitud se registra y se
rechaza. No se puede acceder a la variable de entorno del token de emparejamiento y al tráfico entre el módulo
y Kestrel desde una ubicación fuera del servidor. Sin conocer el valor del token de emparejamiento, un atacante
no puede enviar solicitudes que omitan la comprobación en el middleware de IIS.
El módulo ASP.NET Core con una configuración compartida de IIS
El instalador del módulo ASP.NET Core se ejecuta con los privilegios de la cuenta SYSTEM. Dado que la
cuenta local del sistema no tiene permiso de modificación en la ruta de acceso de recurso compartido que se
usa en la configuración compartida de IIS, el instalador recibe un error de acceso denegado al intentar
configurar los valores del módulo en applicationHost.config en el recurso compartido. Al usar una
configuración compartida de IIS, siga estos pasos:
1. Deshabilite la configuración compartida de IIS.
2. Ejecute el instalador.
3. Exporte el archivo applicationHost.config actualizado al recurso compartido.
4. Vuelva a habilitar la configuración compartida de IIS.

Versión del módulo y registros del instalador de la agrupación de


hospedaje
Para determinar la versión instalada del módulo ASP.NET Core, siga estos pasos:
1. En el sistema de hospedaje, vaya a %windir%\System32\inetsrv.
2. Busque el archivo aspnetcore.dll.
3. Haga clic con el botón derecho en el archivo y seleccione Propiedades en el menú contextual.
4. Seleccione la pestaña Detalles. La versión del archivo y la versión del producto representan la versión
instalada del módulo.
Los registros del instalador de la agrupación de hospedaje del módulo se encuentran en
C:\Users\%UserName%\AppData\Local\Temp. El archivo se llama
dd_DotNetCoreWinSvrHosting__<timestamp>_000_AspNetCoreModule_x64.log.

Ubicaciones del módulo, el esquema y el archivo de configuración


Module
IIS (x86/amd64):
%windir%\System32\inetsrv\aspnetcore.dll
%windir%\SysWOW64\inetsrv\aspnetcore.dll
IIS Express (x86/amd64):
%ProgramFiles%\IIS Express\aspnetcore.dll
%ProgramFiles(x86)%\IIS Express\aspnetcore.dll
Schema
IIS
%windir%\System32\inetsrv\config\schema\aspnetcore_schema.xml
IIS Express
%ProgramFiles%\IIS Express\config\schema\aspnetcore_schema.xml
Configuración
IIS
%windir%\System32\inetsrv\config\applicationHost.config
IIS Express
.vs\config\applicationHost.config
Los archivos se pueden encontrar mediante la búsqueda de aspnetcore.dll en el archivo applicationHost.config.
Para IIS Express, el archivo applicationHost.config no existe de forma predeterminada. El archivo se crea en
<application_root>\.vs\config cuando se inicia cualquier proyecto de aplicación web en la solución de Visual
Studio.
Compatibilidad de IIS de tiempo de desarrollo en
Visual Studio para ASP.NET Core
31/05/2018 • 5 minutes to read • Edit Online

Por Sourabh Shirhatti y Luke Latham


En este artículo se describe la compatibilidad de Visual Studio con la depuración de aplicaciones ASP.NET Core
que se ejecutan detrás de IIS en Windows Server. En este tema se explica cómo habilitar esta característica y
configurar un proyecto.

Requisitos previos
Visual Studio for Windows.
Select the ASP.NET and web development workload.
.Net Core 2.1 SDK

Habilitar IIS
1. Vaya a Panel de control > Programas > Programas y características > Activar o desactivar las
características de Windows (lado izquierdo de la pantalla).
2. Active la casilla Internet Information Services.

La instalación de IIS puede requerir un reinicio del sistema.

Configurar IIS
IIS debe tener un sitio web configurado con lo siguiente:
Un nombre de host que coincida con el nombre de host de la dirección URL del perfil de inicio de la aplicación.
Enlace al puerto 443 con un certificado asignado.
Por ejemplo, el nombre de host de un sitio web agregado se establece en "localhost" (el perfil de inicio también
usará "localhost" más adelante en este tema). El puerto se establece en "443" (HTTPS ). El certificado de
desarrollo de IIS Express se asigna al sitio web, pero cualquier certificado válido sirve:

Si la instalación de IIS ya tiene un sitio web predeterminado con un nombre de host que coincide con el nombre
de host de la dirección URL del perfil de inicio de la aplicación:
Agregue un enlace al puerto 443 (HTTPS ).
Asigne un certificado válido al sitio web.

Habilitación de la compatibilidad con IIS en tiempo de desarrollo en


Visual Studio
1. Inicie el instalador de Visual Studio.
2. Seleccione el componente Compatibilidad con IIS en tiempo de desarrollo. El componente se muestra
como opcional en el panel Resumen de la carga de trabajo Desarrollo de ASP.NET y web. El componente
instala el módulo ASP.NET Core, que es un módulo nativo de IIS necesario para ejecutar aplicaciones ASP.NET
Core detrás de IIS en una configuración de proxy inverso.
Configuración del proyecto
Redireccionamiento de HTTPS
En un nuevo proyecto, active la casilla Configure for HTTPS (Configurar para HTTPS ) en la ventana Nueva
aplicación web ASP.NET Core:

En un proyecto existente, use el Middleware de redireccionamiento HTTPS en Startup.Configure mediante la


llamada al método de extensión UseHttpsRedirection:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();

app.UseMvc();
}

Perfil de inicio de IIS


Cree un nuevo perfil de inicio para agregar la compatibilidad con IIS en tiempo de desarrollo:
1. En Perfil, seleccione el botón Nuevo. Asigne el perfil el nombre "IIS" en la ventana emergente. Seleccione
Aceptar para crear el perfil.
2. En Iniciar, seleccione IIS en la lista.
3. Active la casilla Iniciar explorador y proporcione la dirección URL del punto de conexión. Use el protocolo
HTTPS. Este ejemplo usa https://localhost/WebApplication1 .
4. En la sección Variables de entorno, seleccione el botón Agregar. Proporcione una variable de entorno con
una clave de ASPNETCORE_ENVIRONMENT y un valor de Development .
5. En el área Configuración del servidor web, establezca la dirección URL de la aplicación. Este ejemplo usa
https://localhost/WebApplication1 .
6. Guarde el perfil.
Como alternativa, puede agregar manualmente un perfil de inicio al archivo launchSettings.json en la aplicación:

{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iis": {
"applicationUrl": "https://localhost/WebApplication1",
"sslPort": 0
}
},
"profiles": {
"IIS": {
"commandName": "IIS",
"launchBrowser": true,
"launchUrl": "https://localhost/WebApplication1",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

Ejecución del proyecto


En la interfaz de usuario de VS, establezca el botón Ejecutar en el perfil IIS y seleccione el botón para iniciar la
aplicación:
Visual Studio puede solicitar un reinicio si no se ejecuta como administrador. Si es así, reinicie Visual Studio.
Si se usa un certificado de desarrollo que no es de confianza, el explorador puede pedirle que cree una excepción
para un certificado de esta clase.

Recursos adicionales
Hospedaje de ASP.NET Core en Windows con IIS
Introducción al módulo ASP.NET Core
Referencia de configuración del módulo ASP.NET Core
Aplicación de HTTPS
Módulos de IIS con ASP.NET Core
25/06/2018 • 10 minutes to read • Edit Online

Por Luke Latham


Las aplicaciones ASP.NET Core se hospedan en IIS en una configuración de proxy inverso. Algunos de los
módulos nativos de IIS y todos los módulos administrados de IIS no están disponibles para procesar las
solicitudes para las aplicaciones ASP.NET Core. En muchos casos, ASP.NET Core ofrece una alternativa a las
características de los módulos nativos y administrados de IIS.

Módulos nativos
En la tabla se indican los módulos nativos de IIS que son funcionales con solicitudes de proxy inverso en
aplicaciones ASP.NET Core.

FUNCIONAL CON APLICACIONES ASP.NET


MODULE CORE OPCIÓN DE ASP.NET CORE

Autenticación anónima Sí
AnonymousAuthenticationModule

Autenticación básica Sí
BasicAuthenticationModule

Autenticación de asignaciones de Sí
certificados de cliente
CertificateMappingAuthenticationModule

CGI No
CgiModule

Validación de configuración Sí
ConfigurationValidationModule

Errores HTTP No Middleware de páginas de códigos de


CustomErrorModule estado

Registro personalizado Sí
CustomLoggingModule

Documento predeterminado No Middleware de archivos


DefaultDocumentModule predeterminados

Autenticación implícita Sí
DigestAuthenticationModule

Examen de directorios No Middleware de exploración de


DirectoryListingModule directorios

Compresión dinámica Sí Middleware de compresión de


DynamicCompressionModule respuestas
FUNCIONAL CON APLICACIONES ASP.NET
MODULE CORE OPCIÓN DE ASP.NET CORE

Traza Sí Registro de ASP.NET Core


FailedRequestsTracingModule

Almacenamiento en caché de No Middleware de almacenamiento en


archivos caché de respuestas
FileCacheModule

Almacenamiento en caché HTTP No Middleware de almacenamiento en


HttpCacheModule caché de respuestas

Registro HTTP Sí Registro de ASP.NET Core


HttpLoggingModule Implementaciones: elmah.io, Loggr,
NLog, Serilog

Redireccionamiento HTTP Sí Middleware de reescritura de dirección


HttpRedirectionModule URL

Autenticación de asignaciones de Sí
certificados de cliente IIS
IISCertificateMappingAuthenticationModule

Restricciones de IP y dominio Sí
IpRestrictionModule

Filtros ISAPI Sí Middleware


IsapiFilterModule

ISAPI Sí Middleware
IsapiModule

Compatibilidad con el protocolo Sí


ProtocolSupportModule

Filtrado de solicitudes Sí Middleware de reescritura de dirección


RequestFilteringModule URL IRule

Supervisor de solicitudes Sí
RequestMonitorModule

Reescritura de direcciones URL Sí† Middleware de reescritura de dirección


RewriteModule URL

Inclusiones del lado servidor No


ServerSideIncludeModule

Compresión estática No Middleware de compresión de


StaticCompressionModule respuestas

Contenido estático No Middleware de archivos estáticos


StaticFileModule
FUNCIONAL CON APLICACIONES ASP.NET
MODULE CORE OPCIÓN DE ASP.NET CORE

Almacenamiento en caché de Sí
tokens.
TokenCacheModule

Almacenamiento en caché de URI Sí


UriCacheModule

Autorización de URL Sí Identidad de ASP.NET Core


UrlAuthorizationModule

Autenticación de Windows Sí
WindowsAuthenticationModule

†Los tipos de coincidencia isFile y isDirectory del módulo de reescritura de direcciones URL no funcionan
con las aplicaciones ASP.NET Core debido a los cambios en la estructura de directorios.

Módulos administrados
Los módulos administrados no son funcionales cuando la versión de CLR de .NET del grupo de aplicaciones se
establece en No Managed Code (Sin código administrado). ASP.NET Core ofrece alternativas de middleware en
varios casos.

MODULE OPCIÓN DE ASP.NET CORE

AnonymousIdentification

DefaultAuthentication

FileAuthorization

FormsAuthentication Middleware de autenticación de cookies

OutputCache Middleware de almacenamiento en caché de respuestas

Perfil

RoleManager

ScriptModule 4.0

Sesión Middleware de sesión

UrlAuthorization

UrlMappingsModule Middleware de reescritura de dirección URL

UrlRoutingModule 4.0 Identidad de ASP.NET Core

WindowsAuthentication
Cambios en la aplicación del Administrador de IIS
Al usar el Administrador de IIS para realizar la configuración, cambia el archivo web.config de la aplicación. Si
implementa una aplicación e incluye web.config, los cambios realizados con el Administrador de IIS se
sobrescriben con el archivo web.config implementado. Si se realizan cambios en el archivo web.config del
servidor, copie el archivo web.config actualizado en el servidor en el proyecto local inmediatamente.

Deshabilitación de los módulos de IIS


Si un módulo de IIS configurado en el nivel de servidor debe deshabilitarse en una aplicación, la adición del
archivo web.config de la aplicación puede deshabilitar el módulo. Puede dejar el módulo en su sitio y desactivarlo
mediante un valor de configuración (si está disponible), o quitar el módulo de la aplicación.
Desactivación de módulos
Muchos módulos ofrecen un valor de configuración que les permite deshabilitarse sin quitar el módulo de la
aplicación. Esta es la manera más sencilla y rápida de desactivar un módulo. Por ejemplo, el módulo de
redireccionamiento de HTTP se puede deshabilitar con el elemento <httpRedirect> en web.config:

<configuration>
<system.webServer>
<httpRedirect enabled="false" />
</system.webServer>
</configuration>

Para más información sobre la deshabilitación de los módulos con valores de configuración, siga los vínculos de
la sección sobre elementos secundarios de IIS <system.webServer>.
Eliminación de módulos
Si opta por quitar un módulo con un valor de configuración en web.config, primero desbloquee el módulo y
desbloquee la sección <modules> de web.config:
1. Desbloquee el módulo en el nivel de servidor. Seleccione el servidor de IIS en la barra lateral Conexiones
del Administrador de IIS. Abra Módulos en el área IIS. Seleccione el módulo de la lista. En la barra lateral
Acciones e la derecha, seleccione Desbloquear. Desbloquee tantos módulos como quiera quitar de
web.config más tarde.
2. Implemente la aplicación sin una sección <modules> en web.config. Si una aplicación se implementa con
un archivo web.config que contiene la sección <modules> sin haber desbloqueado primero la sección en
el Administrador de IIS, Configuration Manager produce una excepción al intentar desbloquear la sección.
Por lo tanto, implementar la aplicación sin una sección <modules>.
3. Desbloquee la sección <modules> de web.config. En la barra lateral Conexiones, seleccione el sitio web
en Sitios. En el área Administración, abra el error de configuración. Use los controles de navegación
para seleccionar la sección system.webServer/modules . En la barra lateral Acciones de la derecha,
seleccione Desbloquear la sección.
4. En este punto, se puede agregar una sección <modules> al archivo web.config con un elemento
<remove> para quitar el módulo de la aplicación. Se pueden agregar varios elementos <remove> para
quitar varios módulos. Si se realizan cambios en web.config en el servidor, realice inmediatamente los
mismos cambios en el archivo web.config el proyecto de forma local. Quitar un módulo de esta manera no
afecta al uso del módulo con otras aplicaciones del servidor.
<configuration>
<system.webServer>
<modules>
<remove name="MODULE_NAME" />
</modules>
</system.webServer>
</configuration>

Un módulo de IIS también se puede quitar con Appcmd.exe. Proporcione MODULE_NAME y APPLICATION_NAME en el
comando:

Appcmd.exe delete module MODULE_NAME /app.name:APPLICATION_NAME

Por ejemplo, quite DynamicCompressionModule del sitio web predeterminado:

%windir%\system32\inetsrv\appcmd.exe delete module DynamicCompressionModule /app.name:"Default Web Site"

Configuración mínima del módulo


Los únicos módulos necesarios para ejecutar una aplicación ASP.NET Core son el Módulo de autenticación
anónima y el Módulo ASP.NET Core.

El Módulo de almacenamiento en caché de URI ( UriCacheModule ) permite a IIS almacenar en caché la


configuración del sitio web en el nivel de dirección URL. Sin este módulo, IIS debe leer y analizar la configuración
en cada solicitud, incluso cuando se solicita de forma repetida la misma dirección URL. Como consecuencia de
ello, el rendimiento se reduce considerablemente. Aunque el Módulo de almacenamiento en caché de URI no es
estrictamente necesario para ejecutar una aplicación ASP.NET Core hospedada, es recomendable habilitarlo en
todas las implementaciones de ASP.NET Core.
El Módulo de almacenamiento en caché de HTTP ( HttpCacheModule ) implementa la caché de resultados de IIS y
también la lógica de almacenamiento en caché de los elementos de la caché de HTTP.sys. Sin este módulo, el
contenido ya no se almacena en caché en modo kernel y los perfiles de caché se pasan por alto. Quitar el Módulo
de almacenamiento en caché de HTTP normalmente tiene efectos negativos sobre el rendimiento y el uso de los
recursos. Aunque el Módulo de almacenamiento en caché de HTTP no es estrictamente necesario para ejecutar
una aplicación ASP.NET Core hospedada, es recomendable habilitarlo en todas las implementaciones de
ASP.NET Core.

Recursos adicionales
Hospedaje en Windows con IIS
Introduction to IIS Architectures: Modules in IIS (Introducción a las arquitecturas de IIS: módulos de IIS )
IIS Modules Overview (Introducción a los módulos de IIS )
Customizing IIS 7.0 Roles and Modules (Personalización de los roles y módulos de IIS 7.0)
IIS <system.webServer>
Hospedaje de ASP.NET Core en un servicio de
Windows
31/05/2018 • 7 minutes to read • Edit Online

Por Tom Dykstra


La manera recomendada de hospedar una aplicación ASP.NET Core en Windows sin usar IIS es ejecutarla en un
servicio de Windows. Cuando se hospeda como un servicio de Windows, la aplicación puede iniciarse
automáticamente después de reinicios y bloqueos sin necesidad de intervención humana.
Vea o descargue el código de ejemplo (cómo descargarlo). Para obtener instrucciones sobre cómo ejecutar la
aplicación de ejemplo, vea el archivo README.md del ejemplo.

Requisitos previos
La aplicación debe ejecutarse en el entorno de tiempo de ejecución de .NET Framework. En el archivo
.csproj, especifique los valores adecuados para TargetFramework y RuntimeIdentifier. Por ejemplo:

<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<RuntimeIdentifier>win7-x64</RuntimeIdentifier>
</PropertyGroup>

Al crear un proyecto en Visual Studio, use la plantilla Aplicación ASP.NET Core (.NET Framework).
Si la aplicación recibe las solicitudes de Internet (no solo desde una red interna), debe usar el servidor web
HTTP.sys (antes conocidos como WebListener para las aplicaciones ASP.NET Core 1.x) en lugar de Kestrel.
Se recomienda el uso de IIS como un servidor proxy inverso con Kestrel en implementaciones perimetrales.
Para más información, vea When to use Kestrel with a reverse proxy (Cuándo se debe usar Kestrel con un
proxy inverso).

Primeros pasos
En esta sección se explican los cambios mínimos necesarios para configurar un proyecto de ASP.NET Core
existente para que se ejecute en un servicio.
1. Instale el paquete NuGet Microsoft.AspNetCore.Hosting.WindowsServices.
2. Realice los siguientes cambios en Program.Main .
Llame a host.RunAsService en lugar de a host.Run .
Si el código llama a UseContentRoot , use una ruta de acceso a la ubicación de publicación en lugar de
Directory.GetCurrentDirectory() .
ASP.NET Core 2.x
ASP.NET Core 1.x
public static void Main(string[] args)
{
var pathToExe = Process.GetCurrentProcess().MainModule.FileName;
var pathToContentRoot = Path.GetDirectoryName(pathToExe);

var host = WebHost.CreateDefaultBuilder(args)


.UseContentRoot(pathToContentRoot)
.UseStartup<Startup>()
.Build();

host.RunAsService();
}

3. Publique la aplicación en una carpeta. Use dotnet publish o un perfil de publicación de Visual Studio que
publique en una carpeta.
4. Para probar esto, cree e inicie el servicio.
Abra un shell de comandos con privilegios administrativos para usar la herramienta de la línea de
comandos sc.exe para crear e iniciar un servicio. Si el servicio se llamó MyService, se publicó en c:\svc y
se llamó AspNetCoreService, los comandos son:

sc create MyService binPath="c:\svc\aspnetcoreservice.exe"


sc start MyService

El valor binPath es la ruta de acceso al archivo ejecutable de la aplicación, que incluye el nombre del
archivo ejecutable.

Cuando finalicen estos comandos, vaya a la misma ruta de acceso que cuando se ejecutó como una
aplicación de consola (de forma predeterminada, http://localhost:5000 ):

Proporcionar una forma de ejecutar la aplicación fuera de un servicio


Probar y depurar una aplicación resulta más sencillo cuando se ejecuta fuera de un servicio, por lo que es habitual
agregar código que llame a RunAsService solo bajo determinadas condiciones. Por ejemplo, la aplicación se puede
ejecutar como una aplicación de consola con un argumento de línea de comandos --console o si el depurador
está asociado:
ASP.NET Core 2.x
ASP.NET Core 1.x
public static void Main(string[] args)
{
var isService = true;

if (Debugger.IsAttached || args.Contains("--console"))
{
isService = false;
}

var pathToContentRoot = Directory.GetCurrentDirectory();

if (isService)
{
var pathToExe = Process.GetCurrentProcess().MainModule.FileName;
pathToContentRoot = Path.GetDirectoryName(pathToExe);
}

var webHostArgs = args.Where(arg => arg != "--console").ToArray();

var host = WebHost.CreateDefaultBuilder(webHostArgs)


.UseContentRoot(pathToContentRoot)
.UseStartup<Startup>()
.Build();

if (isService)
{
host.RunAsService();
}
else
{
host.Run();
}
}

Controlar los eventos de inicio y detención


Para controlar los eventos OnStarting , OnStarted y OnStopping , realice los siguientes cambios adicionales:
1. Cree una clase que derive de WebHostService :

internal class CustomWebHostService : WebHostService


{
public CustomWebHostService(IWebHost host) : base(host)
{
}

protected override void OnStarting(string[] args)


{
base.OnStarting(args);
}

protected override void OnStarted()


{
base.OnStarted();
}

protected override void OnStopping()


{
base.OnStopping();
}
}

2. Crear un método de extensión para IWebHost que pase el elemento WebHostService personalizado a
ServiceBase.Run :

public static class WebHostServiceExtensions


{
public static void RunAsCustomService(this IWebHost host)
{
var webHostService = new CustomWebHostService(host);
ServiceBase.Run(webHostService);
}
}

3. En Program.Main , llame al nuevo método de extensión, RunAsCustomService , en lugar de a RunAsService :


ASP.NET Core 2.x
ASP.NET Core 1.x

public static void Main(string[] args)


{
var isService = true;

if (Debugger.IsAttached || args.Contains("--console"))
{
isService = false;
}

var pathToContentRoot = Directory.GetCurrentDirectory();

if (isService)
{
var pathToExe = Process.GetCurrentProcess().MainModule.FileName;
pathToContentRoot = Path.GetDirectoryName(pathToExe);
}

var webHostArgs = args.Where(arg => arg != "--console").ToArray();

var host = WebHost.CreateDefaultBuilder(args)


.UseContentRoot(pathToContentRoot)
.UseStartup<Startup>()
.Build();

if (isService)
{
host.RunAsCustomService();
}
else
{
host.Run();
}
}

Si el código WebHostService personalizado requiere un servicio de inserción de dependencias (como un


registrador), obténgalo de la propiedad Services de IWebHost :
internal class CustomWebHostService : WebHostService
{
private ILogger _logger;

public CustomWebHostService(IWebHost host) : base(host)


{
_logger = host.Services.GetRequiredService<ILogger<CustomWebHostService>>();
}

protected override void OnStarting(string[] args)


{
_logger.LogDebug("OnStarting method called.");
base.OnStarting(args);
}

protected override void OnStarted()


{
_logger.LogDebug("OnStarted method called.");
base.OnStarted();
}

protected override void OnStopping()


{
_logger.LogDebug("OnStopping method called.");
base.OnStopping();
}
}

Escenarios de servidor proxy y equilibrador de carga


Los servicios que interactúan con las solicitudes de Internet o de una red corporativa y están detrás de un proxy o
de un equilibrador de carga podrían requerir configuración adicional. Para más información, vea Configurar
ASP.NET Core para trabajar con servidores proxy y equilibradores de carga.

Agradecimientos
El artículo se escribió con ayuda de fuentes publicadas:
Hosting ASP.NET Core as Windows service (Hospedaje de ASP.NET Core como servicio de Windows)
How to host your ASP.NET Core in a Windows Service (Cómo hospedar ASP.NET Core en un servicio de
Windows)
Hospedar ASP.NET Core en Linux con Nginx
31/05/2018 • 18 minutes to read • Edit Online

Por Sourabh Shirhatti


En esta guía se explica cómo configurar un entorno de ASP.NET Core listo para producción en un servidor de
Ubuntu 16.04.

NOTE
En Ubuntu 14.04, supervisord is es la solución recomendada para supervisar el proceso de Kestrel. systemd no está
disponible en Ubuntu 14.04. Consulte la versión anterior de este documento.

En esta guía:
Se coloca una aplicación ASP.NET Core existente detrás de un servidor proxy inverso.
Se configura el servidor proxy inverso para reenviar las solicitudes al servidor web de Kestrel.
Se garantiza que la aplicación web se ejecute al inicio como un demonio.
Se configura una herramienta de administración de procesos para ayudar a reiniciar la aplicación web.

Requisitos previos
1. Acceso a un servidor de Ubuntu 16.04 con una cuenta de usuario estándar con privilegios sudo
2. Una aplicación ASP.NET Core existente

Copia a través de la aplicación


Ejecute dotnet publish desde el entorno de desarrollo para empaquetar una aplicación en un directorio
independiente que pueda ejecutarse en el servidor.
Copie la aplicación ASP.NET Core en el servidor mediante cualquiera de las herramientas que se integran en el
flujo de trabajo de la organización (por ejemplo, SCP, FTP ). Pruebe la aplicación, por ejemplo:
Ejecute dotnet <app_assembly>.dll desde la línea de comandos.
En un explorador, vaya a http://<serveraddress>:<port> para comprobar que la aplicación funciona en Linux.

Configurar un servidor proxy inverso


Un proxy inverso es una configuración común para trabajar con aplicaciones web dinámicas. Un proxy inverso
finaliza la solicitud HTTP y la reenvía a la aplicación ASP.NET Core.
¿Por qué usar un servidor proxy inverso?
Kestrel resulta muy adecuado para suministrar contenido dinámico de ASP.NET Core. Sin embargo, las
funcionalidades de servicio web no son tan completas como las de los servidores, como IIS, Apache o Nginx. Un
servidor proxy inverso puede descargar trabajo, por ejemplo, suministrar contenido estático, almacenar
solicitudes en caché, comprimir solicitudes y finalizar SSL desde el servidor HTTP. Un servidor proxy inverso
puede residir en un equipo dedicado o se puede implementar junto con un servidor HTTP.
Para los fines de esta guía, se usa una única instancia de Nginx. Se ejecuta en el mismo servidor, junto con el
servidor HTTP. En función de requisitos, se puede elegir una configuración diferente.
Como el proxy inverso reenvía las solicitudes, use el Middleware de encabezados reenviados del paquete
Microsoft.AspNetCore.HttpOverrides. El middleware actualiza Request.Scheme , mediante el encabezado
X-Forwarded-Proto , para que los URI de redireccionamiento y otras directivas de seguridad funcionen
correctamente.
Al usar cualquier tipo de middleware de autenticación, el Middleware de encabezados reenviados debe
ejecutarse primero. Este orden garantiza que el middleware de autenticación puede consumir los valores de
encabezado y generar los URI de redireccionamiento correctos.
ASP.NET Core 2.x
ASP.NET Core 1.x
Invoque el método UseForwardedHeaders en Startup.Configure antes de llamar a UseAuthentication o a un
middleware de esquema de autenticación similar. Configure el middleware para reenviar los encabezados
X-Forwarded-For y X-Forwarded-Proto :

app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});

app.UseAuthentication();

Si no se especifica ningún valor ForwardedHeadersOptions para el software intermedio, los encabezados


predeterminados para reenviar son None .
Podría ser necesario realizar una configuración adicional para las aplicaciones hospedadas detrás de servidores
proxy y equilibradores de carga. Para más información, vea Configurar ASP.NET Core para trabajar con
servidores proxy y equilibradores de carga.
Instalar Nginx

sudo apt-get install nginx

NOTE
Si se van a instalar módulos de Nginx opcionales, podría ser necesario compilar Nginx desde el origen.

Use apt-get para instalar Nginx. El instalador crea un script de inicio de System V que ejecuta Nginx como
demonio al iniciarse el sistema. Puesto que Nginx se ha instalado por primera vez, ejecute lo siguiente para
iniciarlo de forma explícita:

sudo service nginx start

Compruebe que un explorador muestra la página de aterrizaje predeterminada de Nginx.


Configurar Nginx
Para configurar Nginx como un proxy inverso para reenviar solicitudes a la aplicación ASP.NET Core, modifique
/etc/nginx/sites-available/default. Ábralo en un editor de texto y reemplace el contenido por el siguiente:
server {
listen 80;
server_name example.com *.example.com;
location / {
proxy_pass http://localhost:5000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection keep-alive;
proxy_set_header Host $http_host;
proxy_cache_bypass $http_upgrade;
}
}

Cuando no hay ninguna coincidencia de server_name , Nginx usa el servidor predeterminado. Si no se define
ningún servidor predeterminado, el primer servidor del archivo de configuración es el servidor predeterminado.
Como procedimiento recomendado, agregue un servidor predeterminado específico que devuelva un código de
estado 444 en el archivo de configuración. Un ejemplo de configuración del servidor predeterminado es:

server {
listen 80 default_server;
# listen [::]:80 default_server deferred;
return 444;
}

Con el archivo de configuración anterior y el servidor predeterminado, Nginx acepta tráfico público en el puerto
80 con el encabezado de host example.com o *.example.com . Las solicitudes que no coincidan con estos hosts
no se reenviarán al Kestrel. Nginx reenvía las solicitudes coincidentes con Kestrel a http://localhost:5000 . Para
más información, consulte How nginx processes a request (Cómo Nginx procesa una solicitud).

WARNING
Si no se especifica una directiva de server_name adecuada, su aplicación se expone a vulnerabilidades de seguridad. Los
enlaces de carácter comodín de subdominio (por ejemplo, *.example.com ) no presentan este riesgo de seguridad si se
controla todo el dominio primario (a diferencia de *.com , que sí es vulnerable). Vea la sección 5.4 de RFC 7230 para
obtener más información.

Una vez establecida la configuración de Nginx, ejecute sudo nginx -t para comprobar la sintaxis de los archivos
de configuración. Si la prueba del archivo de configuración es correcta, fuerce a Nginx a recopilar los cambios
mediante la ejecución de sudo nginx -s reload .

Supervisión de la aplicación
Nginx ahora está configurado para reenviar las solicitudes realizadas a http://<serveraddress>:80 en la
aplicación de ASP.NET Core que se ejecuta en Kestrel en http://127.0.0.1:5000 . Sin embargo, Nginx no está
configurado para administrar el proceso de Kestrel. Se puede usar systemd para crear un archivo de servicio
para iniciar y supervisar la aplicación web subyacente. systemd es un sistema de inicio que proporciona muchas
características eficaces para iniciar, detener y administrar procesos.
Crear el archivo de servicio
Cree el archivo de definición de servicio:

sudo nano /etc/systemd/system/kestrel-hellomvc.service

Este es un archivo de servicio de ejemplo de la aplicación:


[Unit]
Description=Example .NET Web API App running on Ubuntu

[Service]
WorkingDirectory=/var/aspnetcore/hellomvc
ExecStart=/usr/bin/dotnet /var/aspnetcore/hellomvc/hellomvc.dll
Restart=always
RestartSec=10 # Restart service after 10 seconds if dotnet service crashes
SyslogIdentifier=dotnet-example
User=www-data
Environment=ASPNETCORE_ENVIRONMENT=Production
Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false

[Install]
WantedBy=multi-user.target

Si el usuario www -data no se usó en la configuración, primero se debe crear el usuario aquí definido y
otorgársele la propiedad adecuada de los archivos.
Linux tiene un sistema de archivos que distingue mayúsculas de minúsculas. Al establecer
ASPNETCORE_ENVIRONMENT en "Production", se busca el archivo de configuración
appsettings.Production.json, en lugar de appsettings.production.json.

NOTE
Algunos valores (por ejemplo, cadenas de conexión de SQL) deben ser de escape para que los proveedores de
configuración lean las variables de entorno. Use el siguiente comando para generar un valor de escape correctamente para
su uso en el archivo de configuración:

systemd-escape "<value-to-escape>"

Guarde el archivo y habilite el servicio.

systemctl enable kestrel-hellomvc.service

Inicie el servicio y compruebe que se está ejecutando.

systemctl start kestrel-hellomvc.service


systemctl status kestrel-hellomvc.service

● kestrel-hellomvc.service - Example .NET Web API App running on Ubuntu


Loaded: loaded (/etc/systemd/system/kestrel-hellomvc.service; enabled)
Active: active (running) since Thu 2016-10-18 04:09:35 NZDT; 35s ago
Main PID: 9021 (dotnet)
CGroup: /system.slice/kestrel-hellomvc.service
└─9021 /usr/local/bin/dotnet /var/aspnetcore/hellomvc/hellomvc.dll

Con el proxy inverso configurado y Kestrel administrado a través de systemd, la aplicación web está
completamente configurada y se puede acceder a ella desde un explorador en la máquina local en
http://localhost . También es accesible desde una máquina remota, salvo que haya algún firewall que la pueda
estar bloqueando. Al inspeccionar los encabezados de respuesta, el encabezado Server muestra que Kestrel
suministra la aplicación ASP.NET Core.
HTTP/1.1 200 OK
Date: Tue, 11 Oct 2016 16:22:23 GMT
Server: Kestrel
Keep-Alive: timeout=5, max=98
Connection: Keep-Alive
Transfer-Encoding: chunked

Ver los registros


Dado que la aplicación web que usa Kestrel se administra mediante systemd , todos los procesos y eventos se
registran en un diario centralizado. En cambio, este diario incluye todas las entradas de todos los servicios y
procesos administrados por systemd . Para ver los elementos específicos de kestrel-hellomvc.service , use el
siguiente comando:

sudo journalctl -fu kestrel-hellomvc.service

Para obtener más opciones de filtrado, las opciones de tiempo como --since today , --until 1 hour ago o una
combinación de estas pueden reducir la cantidad de entradas que se devuelven.

sudo journalctl -fu kestrel-hellomvc.service --since "2016-10-18" --until "2016-10-18 04:00"

Protección de la aplicación
Habilitar AppArmor
Linux Security Modules (LSM ) es una plataforma que forma parte del kernel de Linux desde Linux 2.6. LSM
admite diferentes implementaciones de los módulos de seguridad. AppArmor es un LSM que implementa un
sistema de control de acceso obligatorio que permite restringir el programa a un conjunto limitado de recursos.
Asegúrese de que AppArmor está habilitado y configurado correctamente.
Configuración del firewall
Cierre todos los puertos externos que no estén en uso. Uncomplicated Firewall (ufw ) proporciona un front-end
para iptables al proporcionar una interfaz de línea de comandos para configurar el firewall. Compruebe que
ufw está configurado para permitir el tráfico en los puertos necesarios.

sudo apt-get install ufw


sudo ufw enable

sudo ufw allow 80/tcp


sudo ufw allow 443/tcp

Proteger Nginx
La distribución predeterminada de Nginx no habilita SSL. Para habilitar características de seguridad adicionales,
compile desde el origen.
Descargar el origen e instalar las dependencias de compilación
# Install the build dependencies
sudo apt-get update
sudo apt-get install build-essential zlib1g-dev libpcre3-dev libssl-dev libxslt1-dev libxml2-dev libgd2-xpm-
dev libgeoip-dev libgoogle-perftools-dev libperl-dev

# Download Nginx 1.10.0 or latest


wget http://www.nginx.org/download/nginx-1.10.0.tar.gz
tar zxf nginx-1.10.0.tar.gz

Cambiar el nombre de la respuesta de Nginx


Edite src/http/ngx_http_header_filter_module.c:

static char ngx_http_server_string[] = "Server: Web Server" CRLF;


static char ngx_http_server_full_string[] = "Server: Web Server" CRLF;

Configurar las opciones y la compilación


Se necesita la biblioteca PCRE para expresiones regulares. Las expresiones regulares se usan en la directiva de
ubicación de ngx_http_rewrite_module. http_ssl_module agrega compatibilidad con el protocolo HTTPS.
Considere el uso de un firewall de aplicación web como ModSecurity para proteger la aplicación.

./configure
--with-pcre=../pcre-8.38
--with-zlib=../zlib-1.2.8
--with-http_ssl_module
--with-stream
--with-mail=dynamic

Configurar SSL
Configure el servidor para que escuche el tráfico HTTPS en el puerto 443 . Para ello, especifique un
certificado válido emitido por una entidad de certificados (CA) de confianza.
Refuerce la seguridad con algunos de los procedimientos descritos en el siguiente archivo
/etc/nginx/nginx.conf. Entre los ejemplos se incluye la elección de un cifrado más seguro y el
redireccionamiento de todo el tráfico a través de HTTP a HTTPS.
Agregar un encabezado HTTP Strict-Transport-Security (HSTS ) garantiza que todas las solicitudes
siguientes realizadas por el cliente sean solo a través de HTTPS.
No agregue el encabezado Strict-Transport-Security o elija un valor de max-age adecuado si tiene
previsto deshabilitar SSL en el futuro.
Agregue el archivo de configuración /etc/nginx/proxy.conf:

proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 10m;
client_body_buffer_size 128k;
proxy_connect_timeout 90;
proxy_send_timeout 90;
proxy_read_timeout 90;
proxy_buffers 32 4k;

Edite el archivo de configuración /etc/nginx/nginx.conf. El ejemplo contiene las dos secciones http y server en
un archivo de configuración.
http {
include /etc/nginx/proxy.conf;
limit_req_zone $binary_remote_addr zone=one:10m rate=5r/s;
server_tokens off;

sendfile on;
keepalive_timeout 29; # Adjust to the lowest possible value that makes sense for your use case.
client_body_timeout 10; client_header_timeout 10; send_timeout 10;

upstream hellomvc{
server localhost:5000;
}

server {
listen *:80;
add_header Strict-Transport-Security max-age=15768000;
return 301 https://$host$request_uri;
}

server {
listen *:443 ssl;
server_name example.com;
ssl_certificate /etc/ssl/certs/testCert.crt;
ssl_certificate_key /etc/ssl/certs/testCert.key;
ssl_protocols TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
ssl_ecdh_curve secp384r1;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
ssl_stapling on; #ensure your cert is capable
ssl_stapling_verify on; #ensure your cert is capable

add_header Strict-Transport-Security "max-age=63072000; includeSubdomains; preload";


add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;

#Redirects all traffic


location / {
proxy_pass http://hellomvc;
limit_req zone=one burst=10 nodelay;
}
}
}

Proteger Nginx frente al secuestro de clic


El secuestro de clic es una técnica malintencionada para recopilar los clics de un usuario infectado. El secuestro
de clic engaña a la víctima (visitante) para que haga clic en un sitio infectado. Use X-FRAME -OPTIONS para
proteger su sitio.
Edite el archivo nginx.conf:

sudo nano /etc/nginx/nginx.conf

Agregue la línea add_header X-Frame-Options "SAMEORIGIN"; y guarde el archivo, después, reinicie Nginx.
Examen de tipo MIME
Este encabezado evita que la mayoría de los exploradores examinen el MIME de una respuesta fuera del tipo de
contenido declarado, ya que el encabezado indica al explorador que no reemplace el tipo de contenido de
respuesta. Con la opción nosniff , si el servidor indica que el contenido es "text/html", el explorador lo
representa como "text/html".
Edite el archivo nginx.conf:

sudo nano /etc/nginx/nginx.conf

Agregue la línea add_header X-Content-Type-Options "nosniff"; , guarde el archivo y después reinicie Nginx.
Hospedar ASP.NET Core en Linux con Apache
31/05/2018 • 17 minutes to read • Edit Online

Por Shayne Boyer


Mediante esta guía, aprenda a configurar Apache como servidor proxy inverso en CentOS 7 para redirigir el
tráfico HTTP a una aplicación web ASP.NET Core que se ejecuta en Kestrel. La extensión mod_proxy y los
módulos relacionados crean el proxy inverso del servidor.

Requisitos previos
1. Servidor que ejecute CentOS 7, con una cuenta de usuario estándar con privilegios sudo
2. Aplicación ASP.NET Core

Publicar la aplicación
Publique la aplicación como una implementación independiente en la configuración de versión del tiempo de
ejecución de CentOS 7 ( centos.7-x64 ). Copie el contenido de la carpeta bin/Release/netcoreapp2.0/centos.7 -
x64/publish en el servidor mediante SCP, FTP u otro método de transferencia de archivos.

NOTE
En un escenario de implementación de producción, un flujo de trabajo de integración continua lleva a cabo la tarea de
publicar la aplicación y copiar los recursos en el servidor.

Configurar un servidor proxy


Un proxy inverso es una configuración común para trabajar con aplicaciones web dinámicas. El proxy inverso
finaliza la solicitud HTTP y la reenvía a la aplicación ASP.NET.
Un servidor proxy es el que reenvía las solicitudes de cliente a otro servidor en lugar de realizarlas él mismo.
Los proxies inversos las reenvían a un destino fijo, normalmente en nombre de clientes arbitrarios. En esta guía,
Apache se configura como proxy inverso que se ejecuta en el mismo servidor en el que Kestrel atiende la
aplicación ASP.NET Core.
Como el proxy inverso reenvía las solicitudes, use el Middleware de encabezados reenviados del paquete
Microsoft.AspNetCore.HttpOverrides. El middleware actualiza Request.Scheme , mediante el encabezado
X-Forwarded-Proto , para que los URI de redireccionamiento y otras directivas de seguridad funcionen
correctamente.
Al usar cualquier tipo de middleware de autenticación, el Middleware de encabezados reenviados debe
ejecutarse primero. Este orden garantiza que el middleware de autenticación puede consumir los valores de
encabezado y generar los URI de redireccionamiento correctos.
ASP.NET Core 2.x
ASP.NET Core 1.x
Invoque el método UseForwardedHeaders en Startup.Configure antes de llamar a UseAuthentication o a un
middleware de esquema de autenticación similar. Configure el middleware para reenviar los encabezados
X-Forwarded-For y X-Forwarded-Proto :
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});

app.UseAuthentication();

Si no se especifica ningún valor ForwardedHeadersOptions para el software intermedio, los encabezados


predeterminados para reenviar son None .
Podría ser necesario realizar una configuración adicional para las aplicaciones hospedadas detrás de servidores
proxy y equilibradores de carga. Para más información, vea Configurar ASP.NET Core para trabajar con
servidores proxy y equilibradores de carga.
Instalar Apache
Actualice los paquetes de CentOS a sus versiones estables más recientes:

sudo yum update -y

Instale el servidor web de Apache en CentOS con un único comando yum :

sudo yum -y install httpd mod_ssl

Salida de ejemplo después de ejecutar el comando:

Downloading packages:
httpd-2.4.6-40.el7.centos.4.x86_64.rpm | 2.7 MB 00:00:01
Running transaction check
Running transaction test
Transaction test succeeded
Running transaction
Installing : httpd-2.4.6-40.el7.centos.4.x86_64 1/1
Verifying : httpd-2.4.6-40.el7.centos.4.x86_64 1/1

Installed:
httpd.x86_64 0:2.4.6-40.el7.centos.4

Complete!

NOTE
En este ejemplo, la salida refleja httpd.86_64, puesto que la versión de CentOS 7 es de 64 bits. Para comprobar dónde
está instalado Apache, ejecute whereis httpd desde un símbolo del sistema.

Configurar Apache para un proxy inverso


Los archivos de configuración de Apache se encuentran en el directorio /etc/httpd/conf.d/ . Todos los archivos
que tengan la extensión .conf se procesan en orden alfabético, además de los archivos de configuración del
módulo de /etc/httpd/conf.modules.d/ , que contiene todos los archivos de configuración necesarios para
cargar los módulos.
Cree un archivo de configuración denominado hellomvc.conf para la aplicación:
<VirtualHost *:80>
ProxyPreserveHost On
ProxyPass / http://127.0.0.1:5000/
ProxyPassReverse / http://127.0.0.1:5000/
ServerName www.example.com
ServerAlias *.example.com
ErrorLog ${APACHE_LOG_DIR}hellomvc-error.log
CustomLog ${APACHE_LOG_DIR}hellomvc-access.log common
</VirtualHost>

El bloque VirtualHost puede aparecer varias veces en uno o varios archivos en un servidor. En el archivo de
configuración anterior, Apache acepta tráfico público en el puerto 80. El dominio www.example.com se atiende y
el alias *.example.com se resuelve en el mismo sitio web. Para más información, consulte Name-based virtual
host support (Compatibilidad con el host virtual basado en nombres). Las solicitudes se redirigen mediante
proxy en la raíz al puerto 5000 del servidor en 127.0.0.1. Para la comunicación bidireccional, se requieren
ProxyPass y ProxyPassReverse .

WARNING
Si no se especifica una directiva de ServerName correcta en VirtualHost, el bloque expone la aplicación a las
vulnerabilidades de seguridad. Los enlaces de carácter comodín de subdominio (por ejemplo, *.example.com ) no
presentan este riesgo de seguridad si se controla todo el dominio primario (a diferencia de *.com , que sí es vulnerable).
Vea la sección 5.4 de RFC 7230 para obtener más información.

El registro se puede configurar por VirtualHost con las directivas ErrorLog y CustomLog . ErrorLog es la
ubicación donde el servidor registra los errores, y CustomLog establece el nombre de archivo y el formato del
archivo de registro. En este caso, aquí es donde se registra la información de la solicitud. Hay una línea para
cada solicitud.
Guarde el archivo y pruebe la configuración. Si se pasa todo, la respuesta debe ser Syntax [OK] .

sudo service httpd configtest

Reinicie Apache:

sudo systemctl restart httpd


sudo systemctl enable httpd

Supervisión de la aplicación
Apache está configurado ahora para reenviar las solicitudes efectuadas a http://localhost:80 en la aplicación
ASP.NET Core que se ejecuta en Kestrel en http://127.0.0.1:5000 . En cambio, Apache no está configurado para
administrar el proceso de Kestrel. Use systemd y cree un archivo de servicio para iniciar y supervisar la
aplicación web subyacente. systemd es un sistema de inicio que proporciona muchas características eficaces
para iniciar, detener y administrar procesos.
Crear el archivo de servicio
Cree el archivo de definición de servicio:

sudo nano /etc/systemd/system/kestrel-hellomvc.service

Un archivo de servicio de ejemplo para la aplicación:


[Unit]
Description=Example .NET Web API App running on CentOS 7

[Service]
WorkingDirectory=/var/aspnetcore/hellomvc
ExecStart=/usr/local/bin/dotnet /var/aspnetcore/hellomvc/hellomvc.dll
Restart=always
# Restart service after 10 seconds if dotnet service crashes
RestartSec=10
SyslogIdentifier=dotnet-example
User=apache
Environment=ASPNETCORE_ENVIRONMENT=Production

[Install]
WantedBy=multi-user.target

NOTE
Usuario: si el usuario apache no se usa en la configuración, primero se debe crear el usuario y se le debe conceder la
propiedad adecuada para los archivos.

NOTE
Algunos valores (por ejemplo, cadenas de conexión de SQL) deben ser de escape para que los proveedores de
configuración lean las variables de entorno. Use el siguiente comando para generar un valor de escape correctamente para
su uso en el archivo de configuración:

systemd-escape "<value-to-escape>"

Guarde el archivo y habilite el servicio.

systemctl enable kestrel-hellomvc.service

Inicie el servicio y compruebe que se está ejecutando:

systemctl start kestrel-hellomvc.service


systemctl status kestrel-hellomvc.service

● kestrel-hellomvc.service - Example .NET Web API App running on CentOS 7


Loaded: loaded (/etc/systemd/system/kestrel-hellomvc.service; enabled)
Active: active (running) since Thu 2016-10-18 04:09:35 NZDT; 35s ago
Main PID: 9021 (dotnet)
CGroup: /system.slice/kestrel-hellomvc.service
└─9021 /usr/local/bin/dotnet /var/aspnetcore/hellomvc/hellomvc.dll

Con el proxy inverso configurado y Kestrel administrado mediante systemd, la aplicación web está
completamente configurada y se puede acceder a ella desde un explorador en la máquina local en
http://localhost . Inspeccionar los encabezados de respuesta; el encabezado Server indica que Kestrel atiende
la aplicación ASP.NET Core:
HTTP/1.1 200 OK
Date: Tue, 11 Oct 2016 16:22:23 GMT
Server: Kestrel
Keep-Alive: timeout=5, max=98
Connection: Keep-Alive
Transfer-Encoding: chunked

Ver los registros


Dado que la aplicación web que usa Kestrel se administra mediante systemd, los procesos y eventos se registran
en un diario centralizado. Sin embargo, este diario incluye todas las entradas de todos los servicios y procesos
administrados por systemd. Para ver los elementos específicos de kestrel-hellomvc.service , use el siguiente
comando:

sudo journalctl -fu kestrel-hellomvc.service

Para el filtrado de tiempo, especifique las opciones de tiempo con el comando. Por ejemplo, use --since today
para filtrar por el día actual o --until 1 hour ago para ver las entradas de la hora anterior. Para más
información, consulte la página man de journalctl.

sudo journalctl -fu kestrel-hellomvc.service --since "2016-10-18" --until "2016-10-18 04:00"

Protección de la aplicación
Configurar el firewall
Firewalld es un demonio dinámico para administrar el firewall con compatibilidad con zonas de red. Los puertos
y el filtrado de paquetes se pueden seguir administrando mediante iptables. Firewalld debe instalarse de forma
predeterminada. yum puede usarse para instalar el paquete o comprobar que está instalado.

sudo yum install firewalld -y

Use firewalld para abrir solo los puertos necesarios para la aplicación. En este caso se usan los puertos 80 y
443. Los siguientes comandos establecen de forma permanente que se abran los puertos 80 y 443:

sudo firewall-cmd --add-port=80/tcp --permanent


sudo firewall-cmd --add-port=443/tcp --permanent

Vuelva a cargar la configuración del firewall. Compruebe los servicios y puertos disponibles en la zona
predeterminada. Hay opciones disponibles si se inspecciona firewall-cmd -h .

sudo firewall-cmd --reload


sudo firewall-cmd --list-all
public (default, active)
interfaces: eth0
sources:
services: dhcpv6-client
ports: 443/tcp 80/tcp
masquerade: no
forward-ports:
icmp-blocks:
rich rules:

Configuración de SSL
Para configurar Apache para SSL, se usa el módulo mod_ssl. Cuando se instaló el módulo httpd, también lo hizo
el módulo mod_ssl. Si aún no se ha instalado, use yum para agregarlo a la configuración.

sudo yum install mod_ssl

Para usar SSL, instale el módulo mod_rewrite para habilitar la reescritura de direcciones URL:

sudo yum install mod_rewrite

Modifique el archivo hellomvc.conf para permitir la reescritura de direcciones URL y proteger la comunicación
en el puerto 443:

<VirtualHost *:80>
RewriteEngine On
RewriteCond %{HTTPS} !=on
RewriteRule ^/?(.*) https://%{SERVER_NAME}/ [R,L]
</VirtualHost>

<VirtualHost *:443>
ProxyPreserveHost On
ProxyPass / http://127.0.0.1:5000/
ProxyPassReverse / http://127.0.0.1:5000/
ErrorLog /var/log/httpd/hellomvc-error.log
CustomLog /var/log/httpd/hellomvc-access.log common
SSLEngine on
SSLProtocol all -SSLv2
SSLCipherSuite ALL:!ADH:!EXPORT:!SSLv2:!RC4+RSA:+HIGH:+MEDIUM:!LOW:!RC4
SSLCertificateFile /etc/pki/tls/certs/localhost.crt
SSLCertificateKeyFile /etc/pki/tls/private/localhost.key
</VirtualHost>

NOTE
En este ejemplo se usa un certificado generado localmente. SSLCertificateFile debe ser el archivo de certificado principal
para el nombre de dominio. SSLCertificateKeyFile debe ser el archivo de claves generado al crear el CSR.
SSLCertificateChainFile debe ser el archivo de certificado intermedio (si existe) proporcionado por la entidad de
certificación.

Guarde el archivo y pruebe la configuración.

sudo service httpd configtest

Reinicie Apache:
sudo systemctl restart httpd

Sugerencias adicionales de Apache


Encabezados adicionales
Para protegerse frente a ataques malintencionados, hay unos encabezados que se deben modificar o agregar.
Asegúrese de que el módulo mod_headers está instalado.

sudo yum install mod_headers

Protección de Apache de los ataques de secuestro de clic


El secuestro de clic, también conocido como redireccionamiento de interfaz de usuario, es un ataque
malintencionado donde al visitante de un sitio web se le engaña para que haga clic en un vínculo o botón de una
página distinta de la que actualmente está visitando. Use X-FRAME-OPTIONS para proteger el sitio.
Edite el archivo httpd.conf.

sudo nano /etc/httpd/conf/httpd.conf

Agregue la línea Header append X-FRAME-OPTIONS "SAMEORIGIN" . Guarde el archivo. Reinicie Apache.
Examen de tipo MIME
El encabezado impide que Internet Explorer examine MIME (de forma que el elemento
X-Content-Type-Options
Content-Type de un archivo se determina a partir del contenido del archivo). Si el servidor establece el
encabezado Content-Type en text/html con la opción nosniff establecida, Internet Explorer representa el
contenido como text/html sin tener en cuenta el contenido del archivo.
Edite el archivo httpd.conf.

sudo nano /etc/httpd/conf/httpd.conf

Agregue la línea Header set X-Content-Type-Options "nosniff" . Guarde el archivo. Reinicie Apache.
Equilibrio de carga
En este ejemplo se muestra cómo instalar y configurar Apache en CentOS 7 y Kestrel en el mismo equipo de la
instancia. Para no tener un único punto de error, el uso de mod_proxy_balancer y la modificación de
VirtualHost permitirían administrar varias instancias de las aplicaciones web detrás del servidor proxy de
Apache.

sudo yum install mod_proxy_balancer

En el archivo de configuración que se muestra a continuación, se configura una instancia adicional de la


aplicación hellomvc para que se ejecute en el puerto 5001. La sección Proxy se establece con una configuración
de equilibrador con dos miembros para equilibrar la carga byrequests.
<VirtualHost *:80>
RewriteEngine On
RewriteCond %{HTTPS} !=on
RewriteRule ^/?(.*) https://%{SERVER_NAME}/ [R,L]
</VirtualHost>

<VirtualHost *:443>
ProxyPass / balancer://mycluster/

ProxyPassReverse / http://127.0.0.1:5000/
ProxyPassReverse / http://127.0.0.1:5001/

<Proxy balancer://mycluster>
BalancerMember http://127.0.0.1:5000
BalancerMember http://127.0.0.1:5001
ProxySet lbmethod=byrequests
</Proxy>

<Location />
SetHandler balancer
</Location>
ErrorLog /var/log/httpd/hellomvc-error.log
CustomLog /var/log/httpd/hellomvc-access.log common
SSLEngine on
SSLProtocol all -SSLv2
SSLCipherSuite ALL:!ADH:!EXPORT:!SSLv2:!RC4+RSA:+HIGH:+MEDIUM:!LOW:!RC4
SSLCertificateFile /etc/pki/tls/certs/localhost.crt
SSLCertificateKeyFile /etc/pki/tls/private/localhost.key
</VirtualHost>

Límites de velocidad
Use mod_ratelimit, que se incluye en el módulo httpd; el ancho de banda de los clientes puede ser limitado:

sudo nano /etc/httpd/conf.d/ratelimit.conf

En el archivo de ejemplo se limita el ancho de banda a 600 KB/s en la ubicación raíz:

<IfModule mod_ratelimit.c>
<Location />
SetOutputFilter RATE_LIMIT
SetEnv rate-limit 600
</Location>
</IfModule>
Hospedar ASP.NET Core en contenedores de Docker
21/06/2018 • 2 minutes to read • Edit Online

Tiene a su disposición los siguientes artículos para aprender a hospedar aplicaciones de ASP.NET Core en Docker:
Introducción a los contenedores y Docker
Obtenga información sobre la inclusión en contenedores, un enfoque de desarrollo de software en el que una
aplicación o un servicio, sus dependencias y su configuración se empaquetan como una imagen de contenedor.
Puede probar la imagen y, después, implementarla en un host.
¿Qué es Docker?
Descubra Docker, un proyecto de código abierto para automatizar la implementación de aplicaciones como
contenedores portátiles y autosuficientes que se pueden ejecutar en la nube o localmente.
Terminología de Docker
Conozca los términos y las definiciones de la tecnología de Docker.
Contenedores, imágenes y registros de Docker
Descubra cómo se almacenan las imágenes de contenedor de Docker en un registro de imágenes para la
implementación coherente en los entornos.
Creación de imágenes de Docker para aplicaciones de .NET Core
Obtenga información sobre cómo compilar una aplicación de ASP.NET Core y aplicarle Docker. Explore las
imágenes de Docker que mantiene Microsoft y examine los casos de uso.
Visual Studio Tools para Docker
Descubra la manera en que Visual Studio 2017 admite la compilación, la depuración y la ejecución de aplicaciones
de ASP.NET Core destinadas a .NET Framework o .NET Core en Docker para Windows. Se admiten contenedores
de Windows y Linux.
Publicación en una imagen de Docker
Obtenga información sobre cómo usar la extensión de Visual Studio Tools para Docker para implementar una
aplicación de ASP.NET Core en un host de Docker en Azure mediante PowerShell.
Configurar ASP.NET Core para trabajar con servidores proxy y equilibradores de carga
Podría ser necesario realizar una configuración adicional para las aplicaciones hospedadas detrás de servidores
proxy y equilibradores de carga. El proceso de pasar solicitudes a través de un proxy suele ocultar información de
la solicitud original, como la dirección IP de cliente y el esquema. Podría ser necesario reenviar manualmente a la
aplicación cierta información de la solicitud.
Visual Studio Tools para Docker con ASP.NET Core
25/06/2018 • 12 minutes to read • Edit Online

Visual Studio 2017 permite compilar, depurar y ejecutar aplicaciones ASP.NET Core en contenedor destinadas a
.NET Core. Se admiten contenedores de Windows y Linux.

Requisitos previos
Visual Studio 2017 con la carga de trabajo Desarrollo multiplataforma de .NET Core
Docker para Windows

Instalación y configuración
Para la instalación de Docker, revise la información de Docker for Windows: What to know before you install
(Docker para Windows: Información antes de realizar la instalación) e instale Docker para Windows.
Las unidades compartidas de Docker para Windows deben configurarse para admitir la asignación y la
depuración de volúmenes. Haga clic con el botón derecho en el icono de Docker en la bandeja del sistema, haga
clic en Configuración... y seleccione Unidades compartidas. Seleccione la unidad donde los archivos se
almacenan en Docker. Seleccione Aplicar.

TIP
Las versiones 15.6 y posteriores de Visual Studio 2017 le avisan si las unidades compartidas no están configuradas.

Agregar compatibilidad con Docker a una aplicación


Para agregar compatibilidad con Docker a un proyecto de ASP.NET Core, el proyecto debe tener como destino
.NET Core. Se admiten contenedores de Linux y Windows.
Al agregar compatibilidad con Docker a un proyecto, elija un contenedor de Linux o Windows. El host de Docker
debe ejecutar el mismo tipo de contenedor. Para cambiar el tipo de contenedor en la instancia de Docker en
ejecución, haga clic con el botón derecho en el icono de Docker en la bandeja del sistema y elija Switch to
Windows containers... (Cambiar a contenedores Windows) o Switch to Linux containers... (Cambiar a
contenedores Linux).
Nueva aplicación
Al crear una nueva aplicación con las plantillas de proyecto Aplicación web ASP.NET Core, active la casilla
Enable Docker Support (Habilitar compatibilidad con Docker):

Si la plataforma de destino es .NET Core, la lista desplegable de SO permite la selección de un tipo de contenedor.
Aplicación existente
Visual Studio Tools para Docker no admite la adición de Docker a un proyecto de ASP.NET Core existente para
.NET Framework. En los proyectos de ASP.NET Core para .NET Core, hay dos opciones para agregar
compatibilidad con Docker mediante las herramientas. Abra el proyecto en Visual Studio y elija una de las
siguientes opciones:
Seleccione Compatibilidad con Docker en el menú Proyecto.
Haga clic con el botón derecho en el proyecto en el Explorador de soluciones y seleccione Agregar >
Compatibilidad con Docker.

Información general de los recursos de docker


Visual Studio Tools para Docker agrega un proyecto docker-compose a la solución, que contiene lo siguiente:
.dockerignore: contiene una lista de patrones de archivos y directorios que se van a excluir al generar un
contexto de compilación.
docker-compose.yml: archivo base de Docker Compose usado para definir la colección de imágenes que se va a
compilar y ejecutar con docker-compose build y docker-compose run , respectivamente.
docker-compose.override.yml: archivo opcional, leído por Docker Compose, que contiene las invalidaciones de
configuración de los servicios. Visual Studio ejecuta
docker-compose -f "docker-compose.yml" -f "docker-compose.override.yml" para combinar estos archivos.

Se agrega un Dockerfile, la receta para crear una imagen de Docker final, a la raíz del proyecto. Vea Dockerfile
reference (Referencia de Dockerfile) para obtener una descripción de los comandos que contiene. Este Dockerfile
concreto usa una compilación de varias fases con cuatro fases de compilación distintas con nombre:
FROM microsoft/aspnetcore:2.0-nanoserver-1709 AS base
WORKDIR /app
EXPOSE 80

FROM microsoft/aspnetcore-build:2.0-nanoserver-1709 AS build


WORKDIR /src
COPY *.sln ./
COPY HelloDockerTools/HelloDockerTools.csproj HelloDockerTools/
RUN dotnet restore
COPY . .
WORKDIR /src/HelloDockerTools
RUN dotnet build -c Release -o /app

FROM build AS publish


RUN dotnet publish -c Release -o /app

FROM base AS final


WORKDIR /app
COPY --from=publish /app .
ENTRYPOINT ["dotnet", "HelloDockerTools.dll"]

El Dockerfile se basa en la imagen microsoft/aspnetcore. Esta imagen base incluye los paquetes NuGet de
ASP.NET Core, que se han precompilado para mejorar el rendimiento en el inicio.
El archivo docker-compose.yml contiene el nombre de la imagen que se crea al ejecutar el proyecto:

version: '3'

services:
hellodockertools:
image: hellodockertools
build:
context: .
dockerfile: HelloDockerTools\Dockerfile

En el ejemplo anterior, image: hellodockertools genera la imagen hellodockertools:dev cuando se ejecuta la


aplicación en modo de depuración. La imagen hellodockertools:latest se genera cuando se ejecuta la aplicación
en modo de versión.
Si planea colocar la imagen en el Registro, anteponga al nombre de imagen el nombre de usuario de Docker Hub
(por ejemplo, dockerhubusername/hellodockertools ). También puede cambiar el nombre de la imagen para incluir la
dirección URL del Registro privado (por ejemplo, privateregistry.domain.com/hellodockertools ) según la
configuración.

Depuración
Seleccione Docker en la lista desplegable de depuración de la barra de herramientas y empiece a depurar la
aplicación. La vista Docker de la ventana Salida muestra las acciones siguientes en curso:
Se adquiere la imagen microsoft/aspnetcore en tiempo de ejecución (si todavía no está en la caché).
Se adquiere la imagen de compilación o publicación microsoft/aspnetcore-build (si todavía no está en la caché).
La variable de entorno ASPNETCORE_ENVIRONMENT se establece en Development dentro del contenedor.
Se expone el puerto 80 y se asigna a un puerto asignado dinámicamente para el host local. El puerto viene
determinado por el host de Docker y se puede consultar con el comando docker ps .
La aplicación se copia en el contenedor.
Se inicia el explorador predeterminado con el depurador asociado al contenedor, con el puerto asignado
dinámicamente.
La imagen de Docker resultante es la imagen dev de la aplicación, con las imágenes microsoft/aspnetcore como
imagen base. Ejecute el comando docker images en la ventana Consola del Administrador de paquetes (PMC ).
Se muestran las imágenes en la máquina:

REPOSITORY TAG IMAGE ID CREATED SIZE


hellodockertools latest f8f9d6c923e2 About an hour ago 391MB
hellodockertools dev 85c5ffee5258 About an hour ago 389MB
microsoft/aspnetcore-build 2.0-nanoserver-1709 d7cce94e3eb0 15 hours ago 1.86GB
microsoft/aspnetcore 2.0-nanoserver-1709 8872347d7e5d 40 hours ago 389MB

NOTE
La imagen de desarrollo carece del contenido de la aplicación, ya que las configuraciones de depuración usan el montaje de
volumen para proporcionar la experiencia iterativa. Para insertar una imagen, use la configuración de versión.

Ejecute el comando docker ps en la PMC. Tenga en cuenta que la aplicación se ejecuta mediante el contenedor:

CONTAINER ID IMAGE COMMAND CREATED STATUS


PORTS NAMES
baf9a678c88d hellodockertools:dev "C:\\remote_debugge..." 21 seconds ago Up 19 seconds
0.0.0.0:37630->80/tcp dockercompose4642749010770307127_hellodockertools_1

Editar y continuar
Los cambios en archivos estáticos y vistas de Razor se actualizan automáticamente sin necesidad de ningún paso
de compilación. Realice el cambio, guarde y actualice el explorador para ver la actualización.
Las modificaciones en archivos de código requieren compilación y un reinicio del Kestrel dentro del contenedor.
Después de realizar la modificación, presione CTRL + F5 para realizar el proceso e iniciar la aplicación dentro del
contenedor. El contenedor de Docker no se vuelve a compilar ni se detiene. Ejecute el comando docker ps en la
PMC. Observe que el contenedor original se está ejecutando desde hace 10 minutos:

CONTAINER ID IMAGE COMMAND CREATED STATUS


PORTS NAMES
baf9a678c88d hellodockertools:dev "C:\\remote_debugge..." 10 minutes ago Up 10 minutes
0.0.0.0:37630->80/tcp dockercompose4642749010770307127_hellodockertools_1

Publicar imágenes de Docker


Una vez que se completa el ciclo de desarrollo y depuración de la aplicación, Visual Studio Tools para Docker
ayuda a crear la imagen de producción de la aplicación. Cambie la lista desplegable de configuración a Versión y
compile la aplicación. Las herramientas generan la imagen con la etiqueta latest, que puede insertar en el Registro
privado o Docker Hub.
Ejecute el comando docker images en la PMC para ver la lista de imágenes:

REPOSITORY TAG IMAGE ID CREATED SIZE


hellodockertools latest 4cb1fca533f0 19 seconds ago 391MB
hellodockertools dev 85c5ffee5258 About an hour ago 389MB
microsoft/aspnetcore-build 2.0-nanoserver-1709 d7cce94e3eb0 16 hours ago 1.86GB
microsoft/aspnetcore 2.0-nanoserver-1709 8872347d7e5d 40 hours ago 389MB
NOTE
El comando docker images devuelve imágenes de intermediario con los nombres de repositorio y las etiquetas
identificados como <ninguno > (no mencionado anteriormente). Estas imágenes sin nombre son creadas por el Dockerfile de
compilación de varias fases. Mejoran la eficacia de la compilación de la imagen final y solo se vuelven a compilar las capas
necesarias cuando se producen cambios. Cuando las imágenes de intermediario ya no sean necesarias, elimínelas mediante el
comando docker rmi.

Podría esperarse que la imagen de producción o versión fuera más pequeña que la imagen dev. Debido a la
asignación de volumen, el depurador y la aplicación se han ejecutado desde la máquina local y no dentro del
contenedor. La imagen más reciente ha empaquetado el código de aplicación necesario para ejecutar la aplicación
en un equipo host. Por tanto, la diferencia es el tamaño del código de aplicación.
Configuración de ASP.NET Core para trabajar con
servidores proxy y equilibradores de carga
31/05/2018 • 18 minutes to read • Edit Online

Por Luke Latham y Chris Ross


En la configuración recomendada de ASP.NET Core, la aplicación se hospeda mediante IIS/módulo ASP.NET
Core, Nginx o Apache. Los servidores proxy, los equilibradores de carga y otros dispositivos de red con
frecuencia ocultan información sobre la solicitud antes de que llegue a la aplicación:
Cuando las solicitudes HTTPS se redirigen mediante proxy a través de HTTP, el esquema original (HTTPS )
se pierde y se debe reenviar en un encabezado.
Como una aplicación recibe una solicitud del proxy y no desde su verdadero origen en Internet o la red
corporativa, la dirección IP del cliente de origen también se debe reenviar en el encabezado.
Esta información puede ser importante en el procesamiento de las solicitudes, por ejemplo, en los
redireccionamientos, la autenticación, la generación de vínculos, la evaluación de directivas y la ubicación
geográfica del cliente.

Encabezados reenviados
Por costumbre, los servidores proxy reenvían la información en encabezados HTTP.

HEADER DESCRIPTION

X-Forwarded-For Contiene información sobre el cliente que inició la solicitud y


los servidores proxy posteriores en una cadena de
servidores proxy. Este parámetro puede contener
direcciones IP (y, opcionalmente, números de puerto). En
una cadena de servidores proxy, el primer parámetro indica
al cliente dónde se realizó primero la solicitud. Le siguen los
identificadores de proxy posteriores. El último proxy en la
cadena no se encuentra en la lista de parámetros. La última
dirección IP del proxy y, opcionalmente, un número de
puerto, está disponible como la dirección IP remota en la
capa de transporte.

X-Forwarded-Proto El valor del esquema de origen (HTTP/HTTPS). El valor


también puede ser una lista de esquemas si la solicitud ha
pasado por varios servidores proxy.

X-Forwarded-Host El valor original del campo de encabezado de host. Por lo


general, los servidores proxy no modifican el encabezado de
host. Consulte Microsoft Security Advisory CVE-2018-0787
para información sobre una vulnerabilidad de elevación de
privilegios que afecta a sistemas donde el proxy no valida ni
restringe los encabezados de host a valores buenos
conocidos.

El Middleware de encabezados reenviados, del paquete Microsoft.AspNetCore.HttpOverrides, lee estos


encabezados y rellena los campos asociados en HttpContext.
El middleware realiza las siguientes actualizaciones:
HttpContext.Connection.RemoteIpAddress: establézcalo mediante el valor de encabezado X-Forwarded-For .
Los valores de configuración adicionales afectan a cómo el middleware establece RemoteIpAddress . Para
más información, consulte la sección Opciones de Middleware de encabezados reenviados.
HttpContext.Request.Scheme: establézcalo mediante el valor de encabezado X-Forwarded-Proto .
HttpContext.Request.Host: establézcalo mediante el valor de encabezado X-Forwarded-Host .
Tenga en cuenta que no todos los dispositivos de red agregan los encabezados X-Forwarded-For y
X-Forwarded-Proto sin configuración adicional. Consulte las instrucciones del fabricante de su dispositivo si las
solicitudes redirigidas mediante proxy no contienen estos encabezados cuando llegan a la aplicación.
Se pueden configurar los valores predeterminados del Middleware de encabezados reenviados. Estos valores
son:
Solo hay un proxy entre la aplicación y el origen de las solicitudes.
Solo las direcciones de bucle invertido se configuran para servidores proxy conocidos y redes conocidas.

IIS o IIS Express y el módulo ASP.NET Core


El Middleware de encabezados reenviados se habilita de forma predeterminada mediante el Middleware de IIS
Integration cuando la aplicación se ejecuta detrás de IIS y del módulo ASP.NET Core. El Middleware de
encabezados reenviados está activado para ejecutarse primero en la canalización de middleware con una
configuración restringida específica del módulo ASP.NET Core debido a problemas de confianza con los
encabezados reenviados (por ejemplo, suplantación de IP ). El middleware está configurado para reenviar los
encabezados X-Forwarded-For y X-Forwarded-Proto y está restringido a un único proxy localhost. Si se requiere
configuración adicional, consulte la sección Opciones del Middleware de encabezados reenviados.

Otros escenarios de servidor proxy y equilibrador de carga


Al margen del uso del Middleware de IIS Integration, el Middleware de encabezados reenviados no está
habilitado de forma predeterminada. El Middleware de encabezados reenviados debe estar habilitado en una
aplicación para procesar los encabezados reenviados con UseForwardedHeaders. Después de habilitar el
middleware, si no se especifica ForwardedHeadersOptions para él, el valor predeterminado
ForwardedHeadersOptions.ForwardedHeaders es ForwardedHeaders.None.
Configure el middleware con ForwardedHeadersOptions para reenviar los encabezados X-Forwarded-For y
X-Forwarded-Proto en Startup.ConfigureServices . Invoque el método UseForwardedHeaders en
Startup.Configure antes de llamar a otro middleware:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();

services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders =
ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
});
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)


{
app.UseForwardedHeaders();

if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}

app.UseStaticFiles();
// In ASP.NET Core 1.x, replace the following line with: app.UseIdentity();
app.UseAuthentication();
app.UseMvc();
}

NOTE
Si no se especifica ningún valor ForwardedHeadersOptions en Startup.ConfigureServices o directamente para el
método de extensión con UseForwardedHeaders(IApplicationBuilder, ForwardedHeadersOptions), los encabezados
predeterminados que se reenvían son ForwardedHeaders.None. La propiedad
ForwardedHeadersOptions.ForwardedHeaders se debe configurar con los encabezados que se reenvían.

Opciones del Middleware de encabezados reenviados


ForwardedHeadersOptions controla el comportamiento del Middleware de encabezados reenviados:

services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardLimit = 2;
options.KnownProxies.Add(IPAddress.Parse("127.0.10.1"));
options.ForwardedForHeaderName = "X-Forwarded-For-Custom-Header-Name";
});

OPCIÓN DESCRIPTION

ForwardedForHeaderName Use el encabezado especificado por esta propiedad en lugar


del especificado por
ForwardedHeadersDefaults.XForwardedForHeaderName.

El valor predeterminado es X-Forwarded-For .


OPCIÓN DESCRIPTION

ForwardedHeaders Identifica qué reenviadores se deben procesar. Consulte


ForwardedHeaders Enum para obtener la lista de campos
que se aplican. Los valores típicos que se asignan a esta
propiedad son
ForwardedHeaders.XForwardedFor |
ForwardedHeaders.XForwardedProto
.

El valor predeterminado es ForwardedHeaders.None.

ForwardedHostHeaderName Use el encabezado especificado por esta propiedad en lugar


del especificado por
ForwardedHeadersDefaults.XForwardedHostHeaderName.

El valor predeterminado es X-Forwarded-Host .

ForwardedProtoHeaderName Use el encabezado especificado por esta propiedad en lugar


del especificado por
ForwardedHeadersDefaults.XForwardedProtoHeaderName.

El valor predeterminado es X-Forwarded-Proto .

ForwardLimit Limita el número de entradas en los encabezados que se


procesan. Establézcalo en null para deshabilitar el límite,
pero esto solo se debe realizar si están configurados
KnownProxies o KnownNetworks .

El valor predeterminado es 1.

KnownNetworks Intervalos de direcciones de servidores proxy conocidos de


los que se aceptan encabezados reenviados. Proporcione
intervalos de direcciones IP mediante la notación de
Enrutamiento de interdominios sin clases (CIDR).

El valor predeterminado es IList<IPNetwork> que contiene


una única entrada para IPAddress.Loopback .

KnownProxies Direcciones de servidores proxy conocidos de los que se


aceptan encabezados reenviados. Use KnownProxies para
especificar las coincidencias exactas de direcciones IP.

El valor predeterminado es IList<IPAddress> que contiene


una única entrada para IPAddress.IPv6Loopback .

OriginalForHeaderName Use el encabezado especificado por esta propiedad en lugar


del especificado por
ForwardedHeadersDefaults.XOriginalForHeaderName.

El valor predeterminado es X-Original-For .

OriginalHostHeaderName Use el encabezado especificado por esta propiedad en lugar


del especificado por
ForwardedHeadersDefaults.XOriginalForHeaderName.

El valor predeterminado es X-Original-Host .


OPCIÓN DESCRIPTION

OriginalProtoHeaderName Use el encabezado especificado por esta propiedad en lugar


del especificado por
ForwardedHeadersDefaults.XOriginalProtoHeaderName.

El valor predeterminado es X-Original-Proto .

RequireHeaderSymmetry Requiere que el número de valores de encabezado esté


sincronizado entre los valores
ForwardedHeadersOptions.ForwardedHeaders que se van a
procesar.

El valor predeterminado en ASP.NET Core 1.x es true . El


valor predeterminado en ASP.NET Core 2.0 o posterior es
false .

OPCIÓN DESCRIPTION

AllowedHosts Restringe los hosts por el encabezado X-Forwarded-Host a


los valores proporcionados.
Los valores se comparan mediante ordinal-ignore-
case.
Se deben excluir los números de puerto.
Si la lista está vacía, se permiten todos los hosts.
Un carácter comodín de nivel superior * permite
que todos los hosts que no están vacíos.
Se permiten caracteres comodín de subdominio,
pero no coinciden con el dominio raíz. Por ejemplo,
*.contoso.com coincide con el subdominio
foo.contoso.com pero no con el dominio raíz
contoso.com .
Se permiten nombres de host Unicode, pero se
convierten en Punycode para buscar la coincidencia.
Las direcciones IPv6 deben incluir corchetes de
enlace y estar en formato convencional (por ejemplo,
[ABCD:EF01:2345:6789:ABCD:EF01:2345:6789] ).
Las direcciones IPv6 no usan mayúsculas y
minúsculas de forma especial para buscar la igualdad
lógica entre diferentes formatos, y no se realiza
ninguna canonización.
Si no se restringen los hosts permitidos, un atacante
podría suplantar los vínculos generados por el
servicio.
El valor predeterminado es un elemento IList<string> vacío.

ForwardedForHeaderName Use el encabezado especificado por esta propiedad en lugar


del especificado por
ForwardedHeadersDefaults.XForwardedForHeaderName.

El valor predeterminado es X-Forwarded-For .


OPCIÓN DESCRIPTION

ForwardedHeaders Identifica qué reenviadores se deben procesar. Consulte


ForwardedHeaders Enum para obtener la lista de campos
que se aplican. Los valores típicos que se asignan a esta
propiedad son
ForwardedHeaders.XForwardedFor |
ForwardedHeaders.XForwardedProto
.

El valor predeterminado es ForwardedHeaders.None.

ForwardedHostHeaderName Use el encabezado especificado por esta propiedad en lugar


del especificado por
ForwardedHeadersDefaults.XForwardedHostHeaderName.

El valor predeterminado es X-Forwarded-Host .

ForwardedProtoHeaderName Use el encabezado especificado por esta propiedad en lugar


del especificado por
ForwardedHeadersDefaults.XForwardedProtoHeaderName.

El valor predeterminado es X-Forwarded-Proto .

ForwardLimit Limita el número de entradas en los encabezados que se


procesan. Establézcalo en null para deshabilitar el límite,
pero esto solo se debe realizar si están configurados
KnownProxies o KnownNetworks .

El valor predeterminado es 1.

KnownNetworks Intervalos de direcciones de servidores proxy conocidos de


los que se aceptan encabezados reenviados. Proporcione
intervalos de direcciones IP mediante la notación de
Enrutamiento de interdominios sin clases (CIDR).

El valor predeterminado es IList<IPNetwork> que contiene


una única entrada para IPAddress.Loopback .

KnownProxies Direcciones de servidores proxy conocidos de los que se


aceptan encabezados reenviados. Use KnownProxies para
especificar las coincidencias exactas de direcciones IP.

El valor predeterminado es IList<IPAddress> que contiene


una única entrada para IPAddress.IPv6Loopback .

OriginalForHeaderName Use el encabezado especificado por esta propiedad en lugar


del especificado por
ForwardedHeadersDefaults.XOriginalForHeaderName.

El valor predeterminado es X-Original-For .

OriginalHostHeaderName Use el encabezado especificado por esta propiedad en lugar


del especificado por
ForwardedHeadersDefaults.XOriginalForHeaderName.

El valor predeterminado es X-Original-Host .


OPCIÓN DESCRIPTION

OriginalProtoHeaderName Use el encabezado especificado por esta propiedad en lugar


del especificado por
ForwardedHeadersDefaults.XOriginalProtoHeaderName.

El valor predeterminado es X-Original-Proto .

RequireHeaderSymmetry Requiere que el número de valores de encabezado esté


sincronizado entre los valores
ForwardedHeadersOptions.ForwardedHeaders que se van a
procesar.

El valor predeterminado en ASP.NET Core 1.x es true . El


valor predeterminado en ASP.NET Core 2.0 o posterior es
false .

Escenarios y casos de uso


Cuando no es posible agregar encabezados reenviados y todas las solicitudes son seguras
En algunos casos, puede que no sea posible agregar encabezados reenviados a las solicitudes redirigidas
mediante proxy a la aplicación. Si el proxy está forzando a que todas las solicitudes externas públicas sean
HTTPS, el esquema se puede establecer manualmente en Startup.Configure antes de usar cualquier tipo de
middleware:

app.Use((context, next) =>


{
context.Request.Scheme = "https";
return next();
});

Este código puede deshabilitarse con una variable de entorno u otro valor de configuración en un entorno de
desarrollo o ensayo.
Tratar con la ruta de acceso base y los servidores proxy que cambian la ruta de acceso de la solicitud
Algunos servidores proxy pasan la ruta de acceso sin cambios pero con una ruta de acceso base de aplicación
que se debe quitar para que el enrutamiento funcione correctamente. El middleware
UsePathBaseExtensions.UsePathBase divide la ruta de acceso en HttpRequest.Path y la ruta de acceso base de
aplicación en HttpRequest.PathBase.
Si es la ruta de acceso base de aplicación para una ruta de acceso de proxy que se pasa como
/foo
/foo/api/1, el middleware establece Request.PathBase en /foo y Request.Path en /api/1 con el siguiente
comando:

app.UsePathBase("/foo");

La ruta de acceso base y la ruta de acceso original se vuelven a aplicar cuando se llama de nuevo al middleware
en orden inverso. Para más información sobre el procesamiento de pedidos de middleware, consulte
Middleware.
Si el proxy recorta la ruta de acceso (por ejemplo, el reenvío /foo/api/1 a /api/1 ), corrija los
redireccionamientos y los vínculos mediante el establecimiento de la propiedad PathBase de la solicitud:
app.Use((context, next) =>
{
context.Request.PathBase = new PathString("/foo");
return next();
});

Si el proxy va a agregar datos de ruta de acceso, descarte parte de esta ruta para corregir los
redireccionamientos y los vínculos; para ello, use StartsWithSegments(PathString, PathString) y asígnelo a la
propiedad Path:

app.Use((context, next) =>


{
if (context.Request.Path.StartsWithSegments("/foo", out var remainder))
{
context.Request.Path = remainder;
}

return next();
});

Solucionar problemas
Cuando no se reenvíen los encabezados como estaba previsto, habilite el registro. Si los registros no
proporcionan suficiente información para solucionar el problema, enumere los encabezados de solicitud
recibidos por el servidor. Los encabezados se pueden escribir en una respuesta de aplicación mediante
middleware insertado:

public void Configure(IApplicationBuilder app, ILoggerFactory loggerfactory)


{
app.Run(async (context) =>
{
context.Response.ContentType = "text/plain";

// Request method, scheme, and path


await context.Response.WriteAsync(
$"Request Method: {context.Request.Method}{Environment.NewLine}");
await context.Response.WriteAsync(
$"Request Scheme: {context.Request.Scheme}{Environment.NewLine}");
await context.Response.WriteAsync(
$"Request Path: {context.Request.Path}{Environment.NewLine}");

// Headers
await context.Response.WriteAsync($"Request Headers:{Environment.NewLine}");

foreach (var header in context.Request.Headers)


{
await context.Response.WriteAsync($"{header.Key}: " +
$"{header.Value}{Environment.NewLine}");
}

await context.Response.WriteAsync(Environment.NewLine);

// Connection: RemoteIp
await context.Response.WriteAsync(
$"Request RemoteIp: {context.Connection.RemoteIpAddress}");
}
}

Asegúrese de que el servidor reciba los encabezados X-Forwarded-* con los valores esperados. Si hay varios
valores en un encabezado determinado, observe que el Middleware de encabezados reenviados procesa los
encabezados en orden inverso, de derecha a izquierda.
La dirección IP remota original de la solicitud debe coincidir con una entrada de las listas KnownProxies o
KnownNetworks antes de procesar X -Forwarded-For. Esto limita la suplantación de encabezados al no aceptarse
reenviadores de servidores proxy que no son de confianza.

Recursos adicionales
Microsoft Security Advisory CVE -2018-0787: ASP.NET Core Elevation Of Privilege Vulnerability (Microsoft
Security Advisory CVE -2018-0787: Vulnerabilidad de elevación de privilegios de ASP.NET Core)
Perfiles de publicación de Visual Studio para la
implementación de aplicaciones ASP.NET Core
25/06/2018 • 22 minutes to read • Edit Online

Por Sayed Ibrahim Hashimi y Rick Anderson


Este documento se centra en el uso de Visual Studio 2017 para crear y usar perfiles de publicación. Los perfiles de
publicación creados con Visual Studio se pueden ejecutar en MSBuild y en Visual Studio 2017. Vea Publicar una
aplicación web de ASP.NET Core en Azure App Service con Visual Studio para obtener instrucciones sobre la
publicación en Azure.
El siguiente archivo de proyecto se creó con el comando dotnet new mvc :
ASP.NET Core 2.x
ASP.NET Core 1.x

<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" />
</ItemGroup>

<ItemGroup>
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.0" />
</ItemGroup>

</Project>

El atributo <Project> del elemento Sdk lleva a cabo las siguientes tareas:
Importa el archivo de propiedades de $ (MSBuildSDKsPath)\Microsoft.NET.Sdk.Web\Sdk\Sdk.Props al
comienzo.
Importa el archivo targets de $ (MSBuildSDKsPath)\Microsoft.NET.Sdk.Web\Sdk\Sdk.targets al final.
La ubicación predeterminada de MSBuildSDKsPath (con Visual Studio 2017 Enterprise) es la carpeta
%programfiles(x86 )%\Microsoft Visual Studio\2017\Enterprise\MSBuild\Sdks.
El SDK de Microsoft.NET.Sdk.Web depende de:
Microsoft.NET.Sdk.Web.ProjectSystem
Microsoft.NET.Sdk.Publish
Lo que hace que se importen las propiedades y los destinos siguientes:
$ (MSBuildSDKsPath)\Microsoft.NET.Sdk.Web.ProjectSystem\Sdk\Sdk.Props
$ (MSBuildSDKsPath)\Microsoft.NET.Sdk.Web.ProjectSystem\Sdk\Sdk.targets
$ (MSBuildSDKsPath)\Microsoft.NET.Sdk.Publish\Sdk\Sdk.Props
$ (MSBuildSDKsPath)\Microsoft.NET.Sdk.Publish\Sdk\Sdk.targets
Los destinos de publicación importan el conjunto adecuado de destinos en función del método de publicación
usado.
Cuando se carga un proyecto en Visual Studio o MSBuild, se llevan a cabo las siguientes acciones generales:
Compilación del proyecto
Cálculo de los archivos para la publicación
Publicación de los archivos en el destino

Cálculo de los elementos del proyecto


Cuando se carga el proyecto, se calculan los elementos del proyecto (archivos). El atributo item type determina
cómo se procesa el archivo. De forma predeterminada, los archivos .cs se incluyen en la lista de elementos
Compile . Después se compilan los archivos de la lista de elementos Compile .

La lista de elementos Content contiene archivos que se van a publicar, además de los resultados de compilación.
De forma predeterminada, los archivos que coinciden con el patrón wwwroot/** se incluyen en el elemento
Content . El wwwroot/\*\* patrón global coincide con los archivos de la carpeta wwwroot y de las subcarpetas y.
Para agregar explícitamente un archivo a la lista de publicación, agregue el archivo directamente en el archivo
.csproj, como se muestra en Archivos de inclusión.
Al seleccionar el botón Publicar en Visual Studio o al publicar desde la línea de comandos:
Se calculan los elementos/propiedades (los archivos que se deben compilar).
Solo para Visual Studio: se restauran los paquetes NuGet. (la restauración debe ser explícita por parte del
usuario en la CLI).
Se compila el proyecto.
Se calculan los elementos de publicación (los archivos que se deben publicar).
Se publica el proyecto (los archivos calculados se copian en el destino de publicación).
Cuando hace referencia a un proyecto de ASP.NET Core Microsoft.NET.Sdk.Web en el archivo de proyecto, se
coloca un archivo app_offline.htm en la raíz del directorio de la aplicación web. Cuando el archivo está presente, el
módulo de ASP.NET Core cierra correctamente la aplicación y proporciona el archivo app_offline.htm durante la
implementación. Para más información, vea ASP.NET Core Module configuration reference (Referencia de
configuración del módulo de ASP.NET Core).

Publicación básica de línea de comandos


La publicación de línea de comandos funciona en todas las plataformas admitidas de .NET Core y no se requiere
Visual Studio. En los ejemplos siguientes, se ejecuta el comando dotnet publish desde el directorio del proyecto
(que contiene el archivo .csproj). Si no se encuentra en la carpeta del proyecto, pase de forma explícita la ruta de
acceso del archivo del proyecto. Por ejemplo:

dotnet publish C:\Webs\Web1

Ejecute los comandos siguientes para crear y publicar una aplicación web:
ASP.NET Core 2.x
ASP.NET Core 1.x

dotnet new mvc


dotnet publish

El comando dotnet publish produce una salida similar a la siguiente:


C:\Webs\Web1>dotnet publish
Microsoft (R) Build Engine version 15.3.409.57025 for .NET Core
Copyright (C) Microsoft Corporation. All rights reserved.

Web1 -> C:\Webs\Web1\bin\Debug\netcoreapp2.0\Web1.dll


Web1 -> C:\Webs\Web1\bin\Debug\netcoreapp2.0\publish\

La carpeta de publicación predeterminada es bin\$(Configuration)\netcoreapp<version>\publish , El valor


predeterminado de $(Configuration) es Debug. En el ejemplo anterior, el elemento <TargetFramework> es
netcoreapp2.0 .

dotnet publish -h muestra información de ayuda de la publicación.


El siguiente comando especifica una compilación Release y el directorio de publicación:

dotnet publish -c Release -o C:\MyWebs\test

El comando dotnet publish llama a MSBuild, que invoca el destino Publish . Todos los parámetros pasados a
dotnet publish se pasan a MSBuild. El parámetro -c se asigna a la propiedad de MSBuild Configuration . El
parámetro -o se asigna a OutputPath .
Se pueden pasar propiedades de MSBuild mediante cualquiera de los siguientes formatos:
p:<NAME>=<VALUE>
/p:<NAME>=<VALUE>

El comando siguiente publica una compilación Release en un recurso compartido de red:


dotnet publish -c Release /p:PublishDir=//r8/release/AdminWeb

El recurso compartido de red se especifica con barras diagonales (//r8/) y funciona en todas las plataformas
compatibles de .NET Core.
Confirme que la aplicación publicada para la implementación no se está ejecutando. Los archivos de la carpeta
publish quedan bloqueados mientras se ejecuta la aplicación. No se puede llevar a cabo la implementación porque
no se pueden copiar los archivos bloqueados.

Publicar los perfiles


En esta sección se usa Visual Studio 2017 para crear un perfil de publicación. Una vez creado, es posible publicar
desde Visual Studio o la línea de comandos.
Los perfiles de publicación pueden simplificar el proceso de publicación, y puede existir cualquier número de
perfiles. Cree un perfil de publicación en Visual Studio mediante la selección de una de las rutas de acceso
siguientes:
Haga clic con el botón derecho en el Explorador de soluciones y seleccione Publicar.
Seleccione Publicar <nombre_del_proyecto> en el menú Compilar.
Aparece la pestaña Publicar de la página de funcionalidades de la aplicación. Si el proyecto no contiene ningún
perfil de publicación, se muestra la página siguiente:
Cuando se seleccione Carpeta, especifique una ruta de acceso a la carpeta para almacenar los recursos
publicados. La carpeta predeterminada es bin\Release\PublishOutput. Haga clic en el botón Crear perfil para
terminar.
Una vez que se crea un perfil de publicación, la pestaña Publicar cambia. El perfil recién creado aparece en una
lista desplegable. Haga clic en Crear nuevo perfil para crear otro nuevo perfil.
El Asistente para publicación admite los siguientes destinos de publicación:
Azure App Service
Azure Virtual Machines
IIS, FTP, etc. (para cualquier servidor web)
Carpeta
Perfil de importación
Para más información, consulte ¿Qué opciones de publicación son las adecuadas para mí?
Al crear un perfil de publicación con Visual Studio, se crea un archivo de MSBuild
Properties/PublishProfiles/<nombre_del_perfil>.pubxml. Este archivo .pubxml es un archivo de MSBuild que
contiene la configuración de publicación. Este archivo se puede modificar para personalizar el proceso de
compilación y publicación. Este archivo se lee durante el proceso de publicación. <LastUsedBuildConfiguration> es
especial porque es una propiedad global y no debe estar en ningún archivo importado en la compilación.
Consulte MSBuild: how to set the configuration property (MSBuild: Cómo establecer la propiedad de
configuración) para más información.
Al publicar en un destino de Azure, el archivo .pubxml contiene el identificador de suscripción de Azure. Con ese
tipo de destino, no se recomienda agregar este archivo al control de código fuente. Al publicar en un destino que
no sea Azure, es seguro insertar el archivo .pubxml en el repositorio.
La información confidencial (por ejemplo, la contraseña de publicación) se cifra en cada usuario y máquina. Se
almacena en el archivo Properties/PublishProfiles/<nombre_del_perfil>.pubxml.user. Como este archivo puede
contener información confidencial, no se debe insertar en el repositorio de control del código fuente.
Para información general sobre cómo publicar una aplicación web en ASP.NET Core, consulte Hospedaje e
implementación, Las tareas de MSBuild y los destinos necesarios para publicar una aplicación ASP.NET Core son
de código abierto en https://github.com/aspnet/websdk.
dotnet publish puede usar perfiles de publicación, de carpeta, Msdeploy y Kudu:
Carpeta (funciona entre plataformas)

dotnet publish WebApplication.csproj /p:PublishProfile=<FolderProfileName>

MSDeploy (actualmente solo funciona en Windows dado que MSDeploy no es multiplataforma):

dotnet publish WebApplication.csproj /p:PublishProfile=<MsDeployProfileName> /p:Password=<DeploymentPassword>

El paquete de MSDeploy (actualmente solo funciona en Windows dado que MSDeploy no es multiplataforma):

dotnet publish WebApplication.csproj /p:PublishProfile=<MsDeployPackageProfileName>

En los ejemplos anteriores, no pase deployonbuild a dotnet publish .


Para más información, consulte Microsoft.NET.Sdk.Publish.
dotnet publish admite las API de Kudu para publicar en Azure desde cualquier plataforma. La publicación de
Visual Studio admite API de Kudu, pero es compatible con WebSDK para la publicación multiplataforma en
Azure.
Agregue un perfil de publicación a la carpeta Properties/PublishProfiles con el contenido siguiente:

<Project>
<PropertyGroup>
<PublishProtocol>Kudu</PublishProtocol>
<PublishSiteName>nodewebapp</PublishSiteName>
<UserName>username</UserName>
<Password>password</Password>
</PropertyGroup>
</Project>

Ejecute el comando siguiente para descomprimir el contenido de publicación y publicarlo en Azure mediante las
API de Kudu:

dotnet publish /p:PublishProfile=Azure /p:Configuration=Release

Establezca las siguientes propiedades de MSBuild cuando use un perfil de publicación:


DeployOnBuild=true
PublishProfile=<Publish profile name>

Por ejemplo, al efectuar una publicación con un perfil denominado FolderProfile, se puede ejecutar cualquiera de
los comandos siguientes:
dotnet build /p:DeployOnBuild=true /p:PublishProfile=FolderProfile
msbuild /p:DeployOnBuild=true /p:PublishProfile=FolderProfile

Al invocar dotnet build, se llama a msbuild para ejecutar el proceso de compilación y publicación. Llamar a
dotnet build o msbuild es equivalente cuando se pasa un perfil de carpeta. Al llamar a MSBuild directamente en
Windows, se usa la versión .NET Framework de MSBuild. Actualmente, MSDeploy está limitado a los equipos con
Windows para efectuar publicaciones. Al llamar a dotnet build en un perfil que no es de carpeta se invoca a
MSBuild, que usa MSDeploy en los perfiles que no son de carpeta. Al llamar a dotnet build en un perfil que no
es de carpeta, se invoca a MSBuild (mediante MSDeploy), lo cual genera un error (incluso si se ejecuta en una
plataforma de Windows). Para publicar con un perfil que no es de carpeta, llame directamente a MSBuild.
El siguiente perfil de publicación de carpeta se creó con Visual Studio y se publica en un recurso compartido de
red:

<?xml version="1.0" encoding="utf-8"?>


<!--
This file is used by the publish/package process of your Web project.
You can customize the behavior of this process by editing this
MSBuild file.
-->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<WebPublishMethod>FileSystem</WebPublishMethod>
<PublishProvider>FileSystem</PublishProvider>
<LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
<LastUsedPlatform>Any CPU</LastUsedPlatform>
<SiteUrlToLaunchAfterPublish />
<LaunchSiteAfterPublish>True</LaunchSiteAfterPublish>
<ExcludeApp_Data>False</ExcludeApp_Data>
<PublishFramework>netcoreapp1.1</PublishFramework>
<ProjectGuid>c30c453c-312e-40c4-aec9-394a145dee0b</ProjectGuid>
<publishUrl>\\r8\Release\AdminWeb</publishUrl>
<DeleteExistingFiles>False</DeleteExistingFiles>
</PropertyGroup>
</Project>

Tenga en cuenta que <LastUsedBuildConfiguration> está establecido en Release . Al efectuar una publicación en
Visual Studio, el valor de la propiedad de configuración <LastUsedBuildConfiguration> se establece con el valor
cuando se inicia el proceso de publicación. La propiedad de configuración <LastUsedBuildConfiguration> es
especial y no se debe reemplazar en un archivo importado de MSBuild. Esta propiedad se puede invalidar desde
la línea de comandos.
Con la CLI de .NET Core:

dotnet build -c Release /p:DeployOnBuild=true /p:PublishProfile=FolderProfile

Con MSBuild:

msbuild /p:Configuration=Release /p:DeployOnBuild=true /p:PublishProfile=FolderProfile

Publicar en un punto de conexión de MSDeploy desde la línea de


comandos
La publicación se puede realizar mediante la CLI de .NET Core o MSBuild. dotnet publish se ejecuta en el
contexto de .NET Core. El comando msbuild requiere .NET Framework, de forma que se limita a los entornos de
Windows.
La forma más fácil de publicar con MSDeploy consiste en crear primero un perfil de publicación en Visual Studio
2017 y, luego, usar el perfil en la línea de comandos.
En el ejemplo siguiente, se crea una aplicación web de ASP.NET Core (con dotnet new mvc ) y se agrega un perfil
de publicación de Azure con Visual Studio.
Ejecute msbuild desde un Símbolo del sistema para desarrolladores de VS 2017. El Símbolo del sistema para
desarrolladores tiene el archivo msbuild.exe correcto en su ruta de acceso con algunas variables de MSBuild
establecidas.
MSBuild usa la sintaxis siguiente:

msbuild <path-to-project-file> /p:DeployOnBuild=true /p:PublishProfile=<Publish Profile> /p:Username=


<USERNAME> /p:Password=<PASSWORD>

Obtenga el valor Password del archivo <nombre de la publicación>.PublishSettings. Descargue el archivo


.PublishSettings desde:
Explorador de soluciones: haga clic con el botón derecho en la aplicación web y seleccione Descargar perfil
de publicación.
Azure Portal: haga clic en Get publish profile (Obtener perfil de publicación) en el panel Overview
(Información general) de Web Apps.

Username está en el perfil de publicación.


En el siguiente ejemplo se usa el perfil de publicación Web11112 - Web Deploy:

msbuild "C:\Webs\Web1\Web1.csproj" /p:DeployOnBuild=true


/p:PublishProfile="Web11112 - Web Deploy" /p:Username="$Web11112"
/p:Password="<password removed>"

Archivos de exclusión
Al publicar aplicaciones web de ASP.NET Core, se incluyen los artefactos de compilación y el contenido de la
carpeta wwwroot. msbuild admite los patrones globales. Por ejemplo, el siguiente elemento <Content> excluye
todo los archivos de texto (.txt) de la carpeta wwwroot/content y de todas sus subcarpetas.

<ItemGroup>
<Content Update="wwwroot/content/**/*.txt" CopyToPublishDirectory="Never" />
</ItemGroup>

El marcado anterior se puede agregar a un perfil de publicación o al archivo .csproj. Si se agrega al archivo .csproj,
la regla se agrega a todos los perfiles de publicación del proyecto.
El siguiente elemento <MsDeploySkipRules> excluye todos los archivos de la carpeta wwwroot/content:

<ItemGroup>
<MsDeploySkipRules Include="CustomSkipFolder">
<ObjectName>dirPath</ObjectName>
<AbsolutePath>wwwroot\\content</AbsolutePath>
</MsDeploySkipRules>
</ItemGroup>

<MsDeploySkipRules> no eliminará del sitio de implementación los destinos skip. Los archivos y carpetas
destinados a <Content> se eliminan del sitio de implementación. Por ejemplo, suponga que una aplicación web
implementada tenía los siguientes archivos:
Views/Home/About1.cshtml
Views/Home/About2.cshtml
Views/Home/About3.cshtml
Si se agregan los siguientes elementos <MsDeploySkipRules> , esos archivos no se eliminarán en el sitio de
implementación.
<ItemGroup>
<MsDeploySkipRules Include="CustomSkipFile">
<ObjectName>filePath</ObjectName>
<AbsolutePath>Views\\Home\\About1.cshtml</AbsolutePath>
</MsDeploySkipRules>

<MsDeploySkipRules Include="CustomSkipFile">
<ObjectName>filePath</ObjectName>
<AbsolutePath>Views\\Home\\About2.cshtml</AbsolutePath>
</MsDeploySkipRules>

<MsDeploySkipRules Include="CustomSkipFile">
<ObjectName>filePath</ObjectName>
<AbsolutePath>Views\\Home\\About3.cshtml</AbsolutePath>
</MsDeploySkipRules>
</ItemGroup>

Los elementos <MsDeploySkipRules> anteriores impiden que se implementen los archivos omitidos. Una vez que
se implementan esos archivos, no se eliminarán.
El siguiente elemento <Content> elimina los archivos de destino en el sitio de implementación:

<ItemGroup>
<Content Update="Views/Home/About?.cshtml" CopyToPublishDirectory="Never" />
</ItemGroup>

El uso de la implementación de línea de comandos con el elemento <Content> anterior, produce la siguiente
salida:

MSDeployPublish:
Starting Web deployment task from source:
manifest(C:\Webs\Web1\obj\Release\netcoreapp1.1\PubTmp\Web1.SourceManifest.
xml) to Destination: auto().
Deleting file (Web11112\Views\Home\About1.cshtml).
Deleting file (Web11112\Views\Home\About2.cshtml).
Deleting file (Web11112\Views\Home\About3.cshtml).
Updating file (Web11112\web.config).
Updating file (Web11112\Web1.deps.json).
Updating file (Web11112\Web1.dll).
Updating file (Web11112\Web1.pdb).
Updating file (Web11112\Web1.runtimeconfig.json).
Successfully executed Web deployment task.
Publish Succeeded.
Done Building Project "C:\Webs\Web1\Web1.csproj" (default targets).

Archivos de inclusión
El siguiente marcado incluye una carpeta images fuera del directorio del proyecto a la carpeta wwwroot/images
del sitio de publicación:

<ItemGroup>
<_CustomFiles Include="$(MSBuildProjectDirectory)/../images/**/*" />
<DotnetPublishFiles Include="@(_CustomFiles)">
<DestinationRelativePath>wwwroot/images/%(RecursiveDir)%(Filename)%(Extension)</DestinationRelativePath>
</DotnetPublishFiles>
</ItemGroup>

El marcado se puede agregar al archivo .csproj o al perfil de publicación. Si se agrega al archivo .csproj, se incluye
en todos los perfiles de publicación del proyecto.
En el siguiente marcado resaltado se muestra cómo:
Copiar un archivo de fuera del proyecto a la carpeta wwwroot.
Excluir la carpeta wwwroot\Content.
Excluir Views\Home\About2.cshtml.

<?xml version="1.0" encoding="utf-8"?>


<!--
This file is used by the publish/package process of your Web project.
You can customize the behavior of this process by editing this
MSBuild file.
-->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<WebPublishMethod>FileSystem</WebPublishMethod>
<PublishProvider>FileSystem</PublishProvider>
<LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
<LastUsedPlatform>Any CPU</LastUsedPlatform>
<SiteUrlToLaunchAfterPublish />
<LaunchSiteAfterPublish>True</LaunchSiteAfterPublish>
<ExcludeApp_Data>False</ExcludeApp_Data>
<PublishFramework />
<ProjectGuid>afa9f185-7ce0-4935-9da1-ab676229d68a</ProjectGuid>
<publishUrl>bin\Release\PublishOutput</publishUrl>
<DeleteExistingFiles>False</DeleteExistingFiles>
</PropertyGroup>
<ItemGroup>
<ResolvedFileToPublish Include="..\ReadMe2.MD">
<RelativePath>wwwroot\ReadMe2.MD</RelativePath>
</ResolvedFileToPublish>

<Content Update="wwwroot\Content\**\*" CopyToPublishDirectory="Never" />


<Content Update="Views\Home\About2.cshtml" CopyToPublishDirectory="Never" />

</ItemGroup>
</Project>

Vea WebSDK Readme (Archivo Léame de WebSDK) para ver más ejemplos de implementación.

Ejecutar un destino antes o después de la publicación


Los destinos BeforePublish y AfterPublish ejecutan un destino antes o después del destino de publicación.
Agregue los siguientes elementos al perfil de publicación para registrar mensajes de la consola antes y después de
la publicación:

<Target Name="CustomActionsBeforePublish" BeforeTargets="BeforePublish">


<Message Text="Inside BeforePublish" Importance="high" />
</Target>
<Target Name="CustomActionsAfterPublish" AfterTargets="AfterPublish">
<Message Text="Inside AfterPublish" Importance="high" />
</Target>

Publicación en un servidor mediante un certificado que no es de


confianza
Agregue la propiedad <AllowUntrustedCertificate> con un valor de True al perfil de publicación:
<PropertyGroup>
<AllowUntrustedCertificate>True</AllowUntrustedCertificate>
</PropertyGroup>

Servicio Kudu
Para ver los archivos de la implementación de una aplicación web de Azure App Service, use el servicio Kudu.
Anexe el token scm al nombre de la aplicación web. Por ejemplo:

RESOLUCIÓN RESULTADO

http://mysite.azurewebsites.net/ Aplicación web

http://mysite.scm.azurewebsites.net/ Servicio Kudu

Seleccione el elemento de menú Consola de depuración para ver, editar, eliminar o agregar archivos.

Recursos adicionales
Web Deploy (MSDeploy) simplifica la implementación de aplicaciones web y sitios web en servidores de IIS.
https://github.com/aspnet/websdk: problemas de archivos y características de solicitud para la
implementación.
Estructura de directorios de ASP.NET Core
25/06/2018 • 3 minutes to read • Edit Online

Por Luke Latham


En ASP.NET Core, el directorio de aplicaciones publicadas, publish, consta de archivos de aplicación, archivos de
configuración, recursos estáticos, paquetes y el entorno de tiempo de ejecución (para las implementaciones
independientes).

TIPO DE APLICACIÓN ESTRUCTURA DE DIRECTORIOS

Implementación dependiente de marco publish†


logs† (opcional a menos que sea necesario
para recibir registros de stdout)
Views† (aplicaciones MVC; si las vistas no
están precompiladas)
Pages† (aplicaciones MVC o de páginas Razor;
si las páginas no están precompiladas)
wwwroot†
archivos *.dll
<nombre del ensamblado>.deps.json
<nombre del ensamblado>.dll
<nombre del ensamblado>.pdb
<nombre del
ensamblado>.PrecompiledViews.dll
<nombre del
ensamblado>.PrecompiledViews.pdb
<nombre del
ensamblado>.runtimeconfig.json
web.config (implementaciones de IIS)

Implementación independiente publish†


logs† (opcional a menos que sea necesario
para recibir registros de stdout)
refs†
Views† (aplicaciones MVC; si las vistas no
están precompiladas)
Pages† (aplicaciones MVC o de páginas Razor;
si las páginas no están precompiladas)
wwwroot†
archivos *.dll
<nombre del ensamblado>.deps.json
<nombre del ensamblado>.exe
<nombre del ensamblado>.pdb
<nombre del
ensamblado>.PrecompiledViews.dll
<nombre del
ensamblado>.PrecompiledViews.pdb
<nombre del
ensamblado>.runtimeconfig.json
web.config (implementaciones de IIS)

†Indica un directorio
El directorio publish representa la ruta de acceso raíz del contenido, también conocida como la ruta de acceso
base de aplicación, de la implementación. Sea cual sea el nombre que se asigna al directorio publish de la
aplicación implementada en el servidor, su ubicación funciona como la ruta física del servidor a la aplicación
hospedada.
El directorio wwwroot, si existe, solo contiene recursos estáticos.
El directorio registros de stdout se puede crear para la implementación mediante una de las dos estrategias
siguientes:
Agregue el siguiente elemento <Target> al archivo del proyecto:

<Target Name="CreateLogsFolder" AfterTargets="Publish">


<MakeDir Directories="$(PublishDir)Logs"
Condition="!Exists('$(PublishDir)Logs')" />
<WriteLinesToFile File="$(PublishDir)Logs\.log"
Lines="Generated file"
Overwrite="True"
Condition="!Exists('$(PublishDir)Logs\.log')" />
</Target>

El elemento <MakeDir> crea una carpeta Logs vacía en la salida publicada. El elemento usa la propiedad
PublishDir para determinar la ubicación de destino para la creación de la carpeta. Varios métodos de
implementación, como Web Deploy, omiten las carpetas vacías durante la implementación. El elemento
<WriteLinesToFile> genera un archivo en la carpeta Logs, que garantiza la implementación de la carpeta
en el servidor. Tenga en cuenta que puede producirse un error en la creación de carpetas si el proceso de
trabajo no tiene acceso de escritura a la carpeta de destino.
Cree físicamente el directorio Logs en el servidor de la implementación.
El directorio de implementación requiere permisos de lectura y ejecución. El directorio Logs requiere permisos
de lectura y escritura. Otros directorios donde se escriben los archivos requieren permisos de lectura y
escritura.
Referencia de errores comunes de Azure App
Service e IIS con ASP.NET Core
25/06/2018 • 19 minutes to read • Edit Online

Por Luke Latham


La siguiente no es una lista completa de errores. Si se produce un error que aquí no aparece, abra un nuevo
problema con instrucciones detalladas para reproducir el error.
Recopile la siguiente información:
Comportamiento del explorador
Entradas de registro de eventos de la aplicación
Entradas de registro de stdout de módulo ASP.NET Core
Compare la información con los siguientes errores comunes. Si se encuentra una coincidencia, siga los consejos
de solución de problemas.

IMPORTANT
Aviso sobre el uso de las versiones preliminares de ASP.NET Core 2.1
Consulte Implementar una versión preliminar de ASP.NET Core en Azure App Service.

El instalador no puede obtener VC++ Redistributable


Excepción del instalador: 0x80072efd o 0x80072f76 - Error no especificado
Excepción del registro del instalador†: Error 0x80072efd o 0x80072f76: Error al ejecutar el paquete
EXE
†El registro se encuentra en C:\Users\
{USUARIO }\AppData\Local\Temp\dd_DotNetCoreWinSvrHosting__{timestamp}.log.

Solución del problema:


Si el sistema no tiene acceso a Internet al instalar la agrupación de hospedaje, se produce esta excepción
cuando se evita que el instalador obtenga Microsoft Visual C++ 2015 Redistributable. Obtenga un instalador
en el Centro de descarga de Microsoft. Si se produce un error en el instalador, es posible que no reciba el
entorno de tiempo de ejecución de .NET Core necesario para hospedar una implementación dependiente del
marco (FDD ). Si va a hospedar una FDD, confirme que el tiempo de ejecución está instalado en Programas y
características. Si es necesario, puede obtener un instalador de tiempo de ejecución en .NET All Downloads
(.NET Todas las descargas). Después de instalar el runtime, reinicie el sistema o IIS al ejecutar net stop was
/y seguido de net start w3svc desde un símbolo del sistema.

La actualización del sistema operativo ha quitado el módulo ASP.NET


Core de 32 bits
Registro de aplicación: Error al cargar el archivo DLL del módulo
C:\WINDOWS\system32\inetsrv\aspnetcore.dll. Los datos son el error.
Solución del problema:
Los archivos que no son de SO del directorio C:\Windows\SysWOW64\inetsrv no se conservan durante
una actualización del sistema operativo. Si ha instalado el módulo ASP.NET Core antes de una actualización
del sistema operativo y luego se ejecuta AppPool en modo de 32 bits después de una actualización del
sistema operativo, se produce este problema. Después de actualizar el sistema operativo, repare el módulo
ASP.NET Core. Consulte Instalación de la agrupación de hospedaje de .NET Core. Seleccione Reparar
cuando se ejecute el instalador.

Conflictos de plataforma con RID


Explorador: Error HTTP 502.5 - Error en el proceso
Registro de aplicación: La aplicación "MACHINE/WEBROOT/APPHOST/{ASSEMBLY }" con la raíz
física "C:{PATH}' no pudo iniciar el proceso con la línea de comandos '"C:\{PATH}{assembly}.{exe|dll}" ",
ErrorCode = '0x80004005 : ff.
Registro del módulo ASP.NET Core: Excepción no controlada: System.BadImageFormatException: No
se pudo cargar el archivo o ensamblado "{assembly}.dll". Se ha intentado cargar un programa con un
formato incorrecto.
Solución del problema:
Confirme que la aplicación se ejecuta localmente en Kestrel. Un error de proceso puede ser el resultado
de un problema en la aplicación. Para más información, consulte Solución de problemas.
Confirme que <PlatformTarget> en .csproj no entra en conflicto con el RID. Por ejemplo, no especifique
un elemento <PlatformTarget> de x86 ni publique con un RID de win10-x64 , ya sea mediante dotnet
publish -c Release -r win10 -x64 o al establecer el elemento <RuntimeIdentifiers> de .csproj en win10-x64 .
El proyecto se publica sin advertencias ni errores, pero se produce un error con las excepciones
registradas anteriores en el sistema.
Si esta excepción se produce en una implementación de Azure Apps al actualizar una aplicación e
implementar ensamblados más recientes, elimine manualmente todos los archivos de la implementación
anterior. Los ensamblados persistentes no compatibles pueden producir una excepción
System.BadImageFormatException al implementar una aplicación actualizada.

Punto de conexión de URI incorrecto o sitio web detenido


Explorador: ERR_CONNECTION_REFUSED
Registro de aplicación: Sin entrada
Registro del módulo ASP.NET Core: Archivo de registro no creado
Solución del problema:
Confirme que se usa el punto de conexión de URI correcto para la aplicación. Compruebe los enlaces.
Confirme que el sitio web de IIS no está en estado Detenido.

Características de servidor CoreWebEngine o W3SVC deshabilitadas


Excepción de sistema operativo: Se deben instalar las características de IIS 7.0 CoreWebEngine y W3SVC
para usar el módulo ASP.NET Core.
Solución del problema:
Confirme que están habilitados el rol y las características correctos. Vea Configuración de IIS.

Ruta de acceso física de sitio web incorrecta o aplicación que falta


Explorador: 403 Prohibido - Acceso denegado --O BIEN -- 403.14 Prohibido - El servidor web está
configurado para no mostrar una lista de los contenidos de este directorio.
Registro de aplicación: Sin entrada
Registro del módulo ASP.NET Core: Archivo de registro no creado
Solución del problema:
Consulte la opción Configuración básica del sitio web de IIS y la carpeta de la aplicación física. Confirme
que la aplicación está en la carpeta en la ruta de acceso física del sitio web de IIS.

Rol incorrecto, módulo no instalado o permisos incorrectos


Explorador: 500.19 Error interno del servidor - No se puede obtener acceso a la página solicitada
porque los datos de configuración relacionados de la página no son válidos.
Registro de aplicación: Sin entrada
Registro del módulo ASP.NET Core: Archivo de registro no creado
Solución del problema:
Confirme que está habilitado el rol adecuado. Vea Configuración de IIS.
Compruebe Programas & Características y confirme que el módulo Microsoft ASP.NET Core se ha
instalado. Si el módulo Microsoft ASP.NET Core no aparece en la lista de programas instalados,
instálelo. Consulte Instalación de la agrupación de hospedaje de .NET Core.
Asegúrese de que Grupo de aplicaciones > Modelo de proceso > Identidad esté establecido en
ApplicationPoolIdentity o que la identidad personalizada tenga los permisos correctos para acceder a
la carpeta de implementación de la aplicación.

Elemento processPath incorrecto, falta la variable PATH, agrupación


de hospedaje no instalada, sistema o IIS no reiniciado, VC++
Redistributable no instalado o infracción de acceso de dotnet.exe
Explorador: Error HTTP 502.5 - Error en el proceso
Registro de aplicación: La aplicación "MACHINE/WEBROOT/APPHOST/{ASSEMBLY }" con la raíz
física "C:\{PATH}' no pudo iniciar el proceso con la línea de comandos '".{assembly}.exe" ", ErrorCode =
'0x80070002 : 0.
Registro del módulo ASP.NET Core: Archivo de registro creado pero vacío
Solución del problema:
Confirme que la aplicación se ejecuta localmente en Kestrel. Un error de proceso puede ser el resultado
de un problema en la aplicación. Para más información, consulte Solución de problemas.
Compruebe el atributo processPath del elemento <aspNetCore> de web.config para confirmar que es
dotnet para una implementación dependiente del marco (FDD ) o .{assembly }.exe para una
implementación independiente (SCD ).
En el caso de una FDD, dotnet.exe podría no ser accesible a través del valor PATH. Confirme que
*C:\Archivos de programa\dotnet* existe en el valor PATH del sistema.
En el caso de una FDD, dotnet.exe podría no ser accesible para la identidad del usuario del grupo de
aplicaciones. Confirme que la identidad del usuario de AppPool tiene acceso al directorio C:\Archivos de
programa\dotnet. Confirme que no haya ninguna regla de denegación configurada para la identidad del
usuario de AppPool en los directorios C:\Archivos de programa\dotnet y de la aplicación.
Puede que se haya implementado una FDD y que se instalara .NET Core sin reiniciar IIS. Ejecute net stop
was /y seguido de net start w3svc desde un símbolo del sistema para reiniciar el servidor o IIS.
Puede que se haya implementado una FDD sin instalar el entorno de tiempo de ejecución de .NET Core
en el sistema de hospedaje. Si no se ha instalado el entorno de tiempo de ejecución de .NET Core, ejecute
el instalador de la agrupación de hospedaje de .NET Core en el sistema. Consulte Instalación de la
agrupación de hospedaje de .NET Core. Si está intentando instalar el entorno de tiempo de ejecución de
.NET Core en un sistema sin conexión a Internet, puede obtenerlo en .NET All Downloads (.NET Todas las
descargas) y ejecute el instalador de la agrupación de hospedaje para instalar el módulo ASP.NET Core.
Para completar la instalación, reinicie el sistema o IIS mediante la ejecución de net stop was /y seguido
de net start w3svc desde un símbolo del sistema.
Puede que se haya implementado una FDD y que el paquete Microsoft Visual C++ 2015 Redistributable
(x64 ) no esté instalado en el sistema. Obtenga un instalador en el Centro de descarga de Microsoft.

Argumentos incorrectos del elemento <aspNetCore>


Explorador: Error HTTP 502.5 - Error en el proceso
Registro de aplicación: La aplicación "MACHINE/WEBROOT/APPHOST/MY_APPLICATION" con la
raíz física "C:\{PATH}' no pudo iniciar el proceso con la línea de comandos '"dotnet" .{assembly}.dll',
ErrorCode = '0x80004005 : 80008081.
Registro del módulo ASP.NET Core: La aplicación que se va a ejecutar no existe: "PATH{assembly}.dll"
Solución del problema:
Confirme que la aplicación se ejecuta localmente en Kestrel. Un error de proceso puede ser el resultado
de un problema en la aplicación. Para más información, consulte Solución de problemas.
Examine el atributo arguments del elemento <aspNetCore> en web.config para confirmar que (a) es .
{assembly }.dll para una implementación dependiente del marco (FDD ); o (b) no está presente, es una
cadena vacía (arguments="") o una lista de argumentos de la aplicación (arguments="arg1, arg2, ...") para
una implementación independiente (SCD ).

Falta la versión de .NET Framework


Explorador: 502.3 Puerta de enlace incorrecta - Error de conexión al intentar enrutar la solicitud.
Registro de aplicación: ErrorCode = La aplicación "MACHINE/WEBROOT/APPHOST/{ASSEMBLY }"
con la raíz física "C:\{PATH}' no pudo iniciar el proceso con la línea de comandos '"dotnet" .{assembly}.dll",
ErrorCode = '0x80004005 : 80008081.
Registro del módulo ASP.NET Core: Excepción de falta de método, archivo o ensamblado. El método,
el archivo o el ensamblado especificado en la excepción es un método, archivo o ensamblado de .NET
Framework.
Solución del problema:
Instale la versión de .NET Framework que falta en el sistema.
En el caso de una implementación dependiente del marco (FDD ), confirme que tiene instalado el entorno
de tiempo de ejecución correcto en el sistema. Si el proyecto se actualiza de la versión 1.1 a la 2.0, se
implementa en el sistema de hospedaje, y se produce esta excepción, compruebe que el marco 2.0 está en
el sistema de hospedaje.

Grupo de aplicaciones detenido


Explorador: 503 Servicio no disponible
Registro de aplicación: Sin entrada
Registro del módulo ASP.NET Core: Archivo de registro no creado
Solución de problemas
Confirme que el grupo de aplicaciones no está en estado Detenido.

Middleware de IIS Integration no implementado


Explorador: Error HTTP 502.5 - Error en el proceso
Registro de aplicación: La aplicación "MACHINE/WEBROOT/APPHOST/{ASSEMBLY }" con la raíz
física "C:\{PATH}' creó el proceso con la línea de comandos '"C:\{PATH}{assembly}.{exe|dll}" ", pero se ha
bloqueado, no ha respondido o no ha escuchado en el puerto especificado "{PUERTO }", ErrorCode =
"0x800705b4"
Registro del módulo ASP.NET Core: Archivo de registro creado que muestra un funcionamiento
normal.
Solución de problemas
Confirme que la aplicación se ejecuta localmente en Kestrel. Un error de proceso puede ser el resultado
de un problema en la aplicación. Para más información, consulte Solución de problemas.
Confirme lo siguiente:
Se hace referencia al Middleware de IIS Integration mediante la llamada al método UseIISIntegration
en el elemento WebHostBuilder de la aplicación (ASP.NET Core 1.x)
La aplicación usa el método CreateDefaultBuilder (ASP.NET Core 2.x).
Para más información, consulte Hospedaje en ASP.NET Core.

La aplicación secundaria incluye una sección de <controladores>


Explorador: Error HTTP 500.19 - Error interno del servidor
Registro de aplicación: Sin entrada
Registro del módulo ASP.NET Core: archivo de registro creado que muestra un funcionamiento
normal de la aplicación raíz. Archivo de registro no creado para la aplicación secundaria.
Solución de problemas
Confirme que el archivo web.config de la aplicación secundaria no incluye una sección <handlers> .

Ruta de acceso incorrecta al registro de stdout


Explorador: la aplicación responde normalmente.
Registro de aplicación: Advertencia: No se pudo crear stdoutLogFile \?
\C:_apps\app_folder\bin\Release\netcoreapp2.0\win10-
x64\publish\logs\path_doesnt_exist\stdout_8748_201831835937.log, ErrorCode = -2147024893.
Registro del módulo ASP.NET Core: Archivo de registro no creado
Solución de problemas
La ruta de acceso stdoutLogFile especificada en el elemento <aspNetCore> de web.config no existe. Para más
información, consulte la sección Creación y redireccionamiento de registros del tema de referencia de
configuración del módulo ASP.NET Core.

Problema general de configuración de aplicación


Explorador: Error HTTP 502.5 - Error en el proceso
Registro de aplicación: La aplicación "MACHINE/WEBROOT/APPHOST/{ASSEMBLY }" con la raíz
física "C:\{PATH}' creó el proceso con la línea de comandos '"C:\{PATH}{assembly}.{exe|dll}" ", pero se ha
bloqueado, no ha respondido o no ha escuchado en el puerto especificado "{PUERTO }", ErrorCode =
"0x800705b4"
Registro del módulo ASP.NET Core: Archivo de registro creado pero vacío
Solución de problemas
Esta excepción general indica que el proceso no se ha iniciado, probablemente debido a un problema de
configuración de la aplicación. Consulte Estructura de directorios para confirmar que los archivos y las
carpetas implementados de la aplicación son adecuados y que los archivos de configuración de la aplicación
están presentes y contienen la configuración correcta para la aplicación y el entorno. Para más información,
consulte Solución de problemas.
Introducción a la seguridad de ASP.NET Core
21/06/2018 • 6 minutes to read • Edit Online

ASP.NET Core permite a los desarrolladores configurar y administrar con facilidad la seguridad de sus aplicaciones.
ASP.NET Core contiene características para administrar la autenticación, autorización, protección de datos,
cumplimiento de SSL, secretos de aplicación, protección contra falsificación de solicitudes y administración de
CORS. Estas características de seguridad permiten compilar aplicaciones de ASP.NET Core sólidas y seguras.

Características de seguridad de ASP.NET Core


ASP.NET Core proporciona muchas herramientas y bibliotecas para proteger las aplicaciones (por ejemplo,
proveedores de identidades integrados), pero puede usar servicios de identidad de terceros como Facebook,
Twitter y LinkedIn. Con ASP.NET Core, puede administrar con facilidad los secretos de aplicación, que son una
forma de almacenar y usar información confidencial sin tener que exponerla en el código.

Autenticación frente a Autorización


La autenticación es un proceso en el que un usuario proporciona credenciales que después se comparan con las
almacenadas en un sistema operativo, base de datos, aplicación o recurso. Si coinciden, los usuarios se autentican
correctamente y, después, pueden realizar las acciones para las que están autorizados durante un proceso de
autorización. La autorización se refiere al proceso que determina las acciones que un usuario puede realizar.
La autenticación también se puede considerar una manera de entrar en un espacio (como un servidor, base de
datos, aplicación o recurso) mientras que la autorización es qué acciones puede realizar el usuario en qué objetos
de ese espacio (servidor, base de datos o aplicación).

Vulnerabilidades más comunes en software


ASP.NET Core y EF contienen características que ayudan a proteger las aplicaciones y evitar las infracciones de
seguridad. La siguiente lista de vínculos le lleva a documentación en la que se detallan técnicas para evitar las
vulnerabilidades de seguridad más comunes en las aplicaciones web:
Ataques de scripting entre sitios
Ataques por inyección de código SQL
Falsificación de solicitudes entre sitios. (CSRF )
Ataques de redireccionamiento abierto
Hay más vulnerabilidades que debe tener en cuenta. Para más información, vea la sección de este documento
sobre Documentación de seguridad de ASP.NET.

Documentación de seguridad de ASP.NET


Autenticación
Introducción a Identity
Habilitar la autenticación con Facebook, Google y otros proveedores externos
Habilitar la autenticación con WS -Federation
Configuración de la autenticación de Windows
Confirmación de cuentas y recuperación de contraseñas
Autenticación en dos fases con SMS
Uso de la autenticación de cookies sin identidad
Azure Active Directory
Integración de Azure AD en una aplicación web de ASP.NET Core
Llamada a una API web de ASP.NET Core desde una aplicación de WPF con Azure AD
Llamada a una API web en una aplicación web de ASP.NET Core con Azure AD
Una aplicación web de ASP.NET Core con Azure AD B2C
Protección de aplicaciones de ASP.NET Core con IdentityServer4
Autorización
Introducción
Creación de una aplicación con datos de usuario protegidos por autorización
Autorización simple
Autorización basada en roles
Autorización basada en notificaciones
Autorización basada en directivas
Inserción de dependencias en controladores de requisitos
Autorización basada en recursos
Autorización basada en visualizaciones
Limitación de la identidad por esquema
Protección de datos
Introducción a la protección de datos
Introducción a las API de protección de datos
API de consumidor
Información general sobre las API de consumidor
Cadenas de propósito
Jerarquía de propósito y configuración multiempresa
Aplicar un algoritmo hash a las contraseñas
Limitación de la duración de cargas protegidas
Desprotección de cargas cuyas claves se han revocado
Configuración
Configuración de la protección de datos
Configuración predeterminada
Directiva de todo el equipo
Escenarios no compatibles con DI
API de extensibilidad
Extensibilidad de criptografía de núcleo
Extensibilidad de administración de claves
Otras API
Implementación
Detalles de cifrado autenticado
Derivación de subclave y cifrado autenticado
Encabezados de contexto
Administración de claves
Proveedores de almacenamiento de claves
Cifrado de claves en reposo
Inmutabilidad de claves y configuración
Formato de almacenamiento de claves
Proveedores de protección de datos efímeros
Compatibilidad
Reemplazar en ASP.NET
Creación de una aplicación con datos de usuario protegidos por autorización
Almacenamiento seguro de secretos de aplicación en el desarrollo
Proveedor de configuración de Azure Key Vault
Aplicación de SSL
Prevención de ataques de falsificación de solicitudes
Prevención de ataques de redireccionamiento abierto
Prevención de scripting entre sitios
Habilitar solicitudes entre orígenes (CORS )
Compartir cookies entre aplicaciones
Autenticación en ASP.NET Core
21/06/2018 • 2 minutes to read • Edit Online

Opciones de autenticación de OSS de la comunidad


Introducción a Identity
Habilitar la autenticación con Facebook, Google y otros proveedores externos
Habilitar la autenticación con WS -Federation
Habilitar la generación de código QR en Identity
Configuración de la autenticación de Windows
Confirmación de cuentas y recuperación de contraseñas
Autenticación en dos fases con SMS
Uso de la autenticación de cookies sin identidad
Azure Active Directory
Integración de Azure AD en una aplicación web de ASP.NET Core
Integración de Azure AD B2C en una aplicación web de ASP.NET Core dirigida a los clientes
Integración de Azure AD B2C en una API web de ASP.NET Core
Llamada a una API web de ASP.NET Core desde una aplicación de WPF con Azure AD
Llamada a una API web en una aplicación web de ASP.NET Core con Azure AD
Protección de aplicaciones de ASP.NET Core con IdentityServer4
Protección de aplicaciones de ASP.NET Core con la autenticación de Azure App Service (autenticación
sencilla)
Artículos basados en los proyectos creados con cuentas de usuario individuales
Introducción a la identidad de un núcleo de
ASP.NET
22/06/2018 • 16 minutes to read • Edit Online

Por Pranav Rastogi, Rick Anderson, Tom Dykstra, Jon Galloway, Erik Reitan, y Steve Smith
Identidad de ASP.NET Core es un sistema de pertenencia que le permite agregar funcionalidad de inicio de
sesión a la aplicación. Los usuarios pueden crear una cuenta y el inicio de sesión con un nombre de usuario y
contraseña o se puede usar un proveedor de inicio de sesión externo como Facebook, Google, Microsoft
Account, Twitter u otras personas.
Puede configurar ASP.NET Core Identity para utilizar una base de datos de SQL Server para almacenar
nombres de usuario, contraseñas y datos de perfil. Como alternativa, puede usar su propio almacén
persistente, por ejemplo, un almacenamiento de tablas de Azure. Este documento contiene instrucciones para
Visual Studio y para el uso de la CLI.
Ver o descargar el código de ejemplo. (Cómo descargar)

Información general de identidad


En este tema, podrá aprender a usar ASP.NET Core Identity para agregar funcionalidad a registrar, inicie
sesión y cierra la sesión un usuario. Para obtener instrucciones detalladas acerca de cómo crear aplicaciones
con ASP.NET Core Identity, vea la sección pasos siguientes al final de este artículo.
1. Cree un proyecto de aplicación Web de ASP.NET Core con cuentas de usuario individuales.
Visual Studio
CLI de .NET Core
En Visual Studio, seleccione archivo > New > proyecto. Seleccione aplicación Web de ASP.NET
Core y haga clic en Aceptar.
Seleccione un ASP.NET Core aplicación Web (Model-View-Controller) para ASP.NET Core 2.x, a
continuación, seleccione Cambiar autenticación.

Un cuadro de diálogo aparece oferta las opciones de autenticación. Seleccione cuentas de usuario
individuales y haga clic en Aceptar para volver al cuadro de diálogo anterior.
Seleccionar cuentas de usuario individuales indica a Visual Studio para crear modelos, ViewModels,
vistas, controladores y otros recursos necesarios para la autenticación como parte de la plantilla de
proyecto.

2. Configurar servicios de identidad y agregar middleware en Startup .


Los servicios de identidad se agregan a la aplicación en el ConfigureServices método en la Startup
clase:
ASP.NET Core 2.x
ASP.NET Core 1.x
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();

services.Configure<IdentityOptions>(options =>
{
// Password settings
options.Password.RequireDigit = true;
options.Password.RequiredLength = 8;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = true;
options.Password.RequireLowercase = false;
options.Password.RequiredUniqueChars = 6;

// Lockout settings
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30);
options.Lockout.MaxFailedAccessAttempts = 10;
options.Lockout.AllowedForNewUsers = true;

// User settings
options.User.RequireUniqueEmail = true;
});

services.ConfigureApplicationCookie(options =>
{
// Cookie settings
options.Cookie.HttpOnly = true;
options.ExpireTimeSpan = TimeSpan.FromMinutes(30);
// If the LoginPath isn't set, ASP.NET Core defaults
// the path to /Account/Login.
options.LoginPath = "/Account/Login";
// If the AccessDeniedPath isn't set, ASP.NET Core defaults
// the path to /Account/AccessDenied.
options.AccessDeniedPath = "/Account/AccessDenied";
options.SlidingExpiration = true;
});

// Add application services.


services.AddTransient<IEmailSender, EmailSender>();

services.AddMvc();
}

Estos servicios se ponen a disposición a la aplicación a través de inyección de dependencia.


Identidad está habilitada para la aplicación mediante una llamada a UseAuthentication en el
Configure método. UseAuthentication Agrega autenticación middleware a la canalización de solicitud.
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}

app.UseStaticFiles();

app.UseAuthentication();

app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}

Para obtener más información sobre el proceso de inicio aplicación, consulte inicio de la aplicación.
3. Cree un usuario.
Inicie la aplicación y, a continuación, haga clic en el registrar vínculo.
Si se trata de la primera vez que se va a realizar esta acción, puede ser necesario para ejecutar las
migraciones. La aplicación le pide que migraciones aplicar. Si es necesario, actualice la página.

Como alternativa, puede probar mediante ASP.NET Core Identity con la aplicación sin una base de
datos persistente desde una base de datos en memoria. Para usar una base de datos en memoria,
agregue el Microsoft.EntityFrameworkCore.InMemory el paquete a la aplicación y modificar la llamada de
la aplicación a AddDbContext en ConfigureServices como se indica a continuación:

services.AddDbContext<ApplicationDbContext>(options =>
options.UseInMemoryDatabase(Guid.NewGuid().ToString()));

Cuando el usuario hace clic en el registrar vínculo, el Register acción se invoca en AccountController
. El Register acción crea el usuario mediante una llamada a CreateAsync en el _userManager objeto
(proporcionado a AccountController por inyección de dependencia):
//
// POST: /Account/Register
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Register(RegisterViewModel model)
{
if (ModelState.IsValid)
{
var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
var result = await _userManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
// For more information on how to enable account confirmation and password reset please
visit http://go.microsoft.com/fwlink/?LinkID=532713
// Send an email with this link
//var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
//var callbackUrl = Url.Action("ConfirmEmail", "Account", new { userId = user.Id, code
= code }, protocol: HttpContext.Request.Scheme);
//await _emailSender.SendEmailAsync(model.Email, "Confirm your account",
// "Please confirm your account by clicking this link: <a href=\"" + callbackUrl +
"\">link</a>");
await _signInManager.SignInAsync(user, isPersistent: false);
_logger.LogInformation(3, "User created a new account with password.");
return RedirectToAction(nameof(HomeController.Index), "Home");
}
AddErrors(result);
}

// If we got this far, something failed, redisplay form


return View(model);
}

Si el usuario se creó correctamente, el usuario se registra en la llamada a _signInManager.SignInAsync .


Nota: vea cuenta confirmación para conocer los pasos evitar el inicio de sesión de inmediato en el
registro.
4. Inicia sesión.
Los usuarios pueden iniciar sesión, haga clic en el sesión vínculo en la parte superior del sitio, o puede
navegar a la página de inicio de sesión si éstos intentan obtener acceso a una parte del sitio que
requiera una autorización. Cuando el usuario envía el formulario en la página de inicio de sesión, el
AccountController Login acción se denomina.

El Loginacción llamadas PasswordSignInAsync en el _signInManager objeto (proporcionado a


AccountController por inyección de dependencia).
//
// POST: /Account/Login
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
{
ViewData["ReturnUrl"] = returnUrl;
if (ModelState.IsValid)
{
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
var result = await _signInManager.PasswordSignInAsync(model.Email,
model.Password, model.RememberMe, lockoutOnFailure: false);
if (result.Succeeded)
{
_logger.LogInformation(1, "User logged in.");
return RedirectToLocal(returnUrl);
}
if (result.RequiresTwoFactor)
{
return RedirectToAction(nameof(SendCode), new { ReturnUrl = returnUrl, RememberMe =
model.RememberMe });
}
if (result.IsLockedOut)
{
_logger.LogWarning(2, "User account locked out.");
return View("Lockout");
}
else
{
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return View(model);
}
}

// If we got this far, something failed, redisplay form


return View(model);
}

La base de Controller clase expone un User propiedad que se puede acceder desde los métodos de
controlador. Por ejemplo, puede enumerar User.Claims y tomar decisiones de autorización. Para
obtener más información, consulte autorización.
5. Cierre sesión.
Al hacer clic en el cerrar sesión vincular llamadas el LogOut acción.

//
// POST: /Account/LogOut
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> LogOut()
{
await _signInManager.SignOutAsync();
_logger.LogInformation(4, "User logged out.");
return RedirectToAction(nameof(HomeController.Index), "Home");
}

El código anterior por encima de las llamadas del _signInManager.SignOutAsync método. El


SignOutAsync método borra las solicitudes del usuario almacenadas en una cookie.

6. Configuración.
Identidad tiene algunos comportamientos predeterminados que se pueden invalidar en la clase de inicio de la
aplicación. IdentityOptions no es necesario configurar al utilizar los comportamientos predeterminados. El
código siguiente establece varias opciones de seguridad de contraseña:
ASP.NET Core 2.x
ASP.NET Core 1.x

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();

services.Configure<IdentityOptions>(options =>
{
// Password settings
options.Password.RequireDigit = true;
options.Password.RequiredLength = 8;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = true;
options.Password.RequireLowercase = false;
options.Password.RequiredUniqueChars = 6;

// Lockout settings
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30);
options.Lockout.MaxFailedAccessAttempts = 10;
options.Lockout.AllowedForNewUsers = true;

// User settings
options.User.RequireUniqueEmail = true;
});

services.ConfigureApplicationCookie(options =>
{
// Cookie settings
options.Cookie.HttpOnly = true;
options.ExpireTimeSpan = TimeSpan.FromMinutes(30);
// If the LoginPath isn't set, ASP.NET Core defaults
// the path to /Account/Login.
options.LoginPath = "/Account/Login";
// If the AccessDeniedPath isn't set, ASP.NET Core defaults
// the path to /Account/AccessDenied.
options.AccessDeniedPath = "/Account/AccessDenied";
options.SlidingExpiration = true;
});

// Add application services.


services.AddTransient<IEmailSender, EmailSender>();

services.AddMvc();
}

Para obtener más información acerca de cómo configurar la identidad, vea configurar identidad.
También puede configurar el tipo de datos de la clave principal, vea tipo de datos de las claves principales de
configurar la identidad.
7. Ver la base de datos.
Si la aplicación usa una base de datos de SQL Server (el valor predeterminado en Windows y para
usuarios de Visual Studio), puede ver la base de datos de la aplicación creada. Puede usar SQL Server
Management Studio. O bien, desde Visual Studio, seleccione vista > Explorador de objetos de
SQL Server. Conectarse a (localdb) \MSSQLLocalDB. La base de datos con un nombre que
coincida con aspnet - <nombre del proyecto>-<cadena de fecha > se muestra.

Expanda la base de datos y su tablas, a continuación, haga clic en el dbo. AspNetUsers de tabla y
seleccione ver datos.
8. Compruebe que funciona de identidad
El valor predeterminado aplicación Web de ASP.NET Core plantilla de proyecto permite a los usuarios
tener acceso a cualquier acción en la aplicación sin necesidad de inicio de sesión. Para comprobar que
funciona ASP.NET Identity, agregue un [Authorize] atribuir a la About acción de la Home controlador.

[Authorize]
public IActionResult About()
{
ViewData["Message"] = "Your application description page.";
return View();
}

Visual Studio
CLI de .NET Core
Ejecutar el proyecto mediante Ctrl + F5 y navegue hasta la sobre página. Solo los usuarios
autenticados pueden tener acceso a la sobre página ahora, por lo que ASP.NET le redirige a la página
de inicio de sesión para iniciar sesión o regístrese.

Componentes de identidad
El ensamblado de referencia principal para el sistema de identidades Microsoft.AspNetCore.Identity . Este
paquete contiene el conjunto básico de interfaces para ASP.NET Core Identity y se incluye por
Microsoft.AspNetCore.Identity.EntityFrameworkCore .

Estas dependencias necesarios para usar el sistema de identidades en aplicaciones de ASP.NET Core:
Microsoft.AspNetCore.Identity.EntityFrameworkCore -Contiene los tipos necesarios para usar la
identidad con Entity Framework Core.
Microsoft.EntityFrameworkCore.SqlServer-Entity Framework Core es la tecnología de acceso a datos
recomendado de Microsoft para las bases de datos relacional como SQL Server. Para las pruebas,
puede usar Microsoft.EntityFrameworkCore.InMemory .
Microsoft.AspNetCore.Authentication.Cookies -Middleware que permite que una aplicación utilizar la
autenticación basada en cookies.

Migrar a la identidad de ASP.NET Core


Para obtener información adicional e instrucciones sobre cómo migrar su identidad existente store vea migrar
autenticación e identidad.

Configuración de seguridad de la contraseña


Vea configuración para obtener un ejemplo que establece los requisitos de contraseña mínima.

Pasos siguientes
Migrar de autenticación e identidad
Confirmación de cuentas y recuperación de contraseñas
Autenticación en dos fases con SMS
Facebook, Google y la autenticación de proveedor externo.
Identidad de scaffolding en proyectos de ASP.NET
Core
22/06/2018 • 18 minutes to read • Edit Online

Por Rick Anderson


ASP.NET Core 2.1 y versiones posteriores proporciona ASP.NET Core Identity como un biblioteca de clases de
Razor. Las aplicaciones que incluyen la identidad pueden aplicar el scaffolder para agregar el código de fuente
contenido en la biblioteca de clase de Razor de identidad (RCL ) de forma selectiva. Puede generar código fuente
para que pueda modificar el código y cambiar el comportamiento. Por ejemplo, podría indicar a la scaffolder para
generar el código que se utiliza en el registro. Código generado tiene prioridad sobre el mismo código en el RCL
de identidad. Para obtener el control total de la interfaz de usuario y no utilice el valor predeterminado RCL, vea la
sección crear origen de interfaz de usuario de identidad completa.
Las aplicaciones que no incluyen autenticación puede aplicar el scaffolder para agregar el paquete de identidad
RCL. Tiene la opción de seleccionar el código de identidad que se genere.
Aunque el scaffolder genera la mayoría del código necesario, tendrá que actualizar el proyecto para completar el
proceso. Este documento explican los pasos necesarios para completar una actualización de la técnica scaffolding
de identidad.
Cuando se ejecuta el scaffolder de identidad, un ScaffoldingReadme.txt archivo se crea en el directorio del
proyecto. El ScaffoldingReadme.txt archivo contiene instrucciones generales sobre lo que se necesita para
completar la actualización de la técnica scaffolding de identidad. Este documento contiene instrucciones más
completas que la ScaffoldingReadme.txt archivo.
Se recomienda utilizar un sistema de control de código fuente que se muestran las diferencias de archivo y le
permite revertir los cambios. Inspeccionar los cambios después de ejecutar al scaffolder de identidad.

Identidad de scaffolding en un proyecto vacío


Run the Identity scaffolder:
Visual Studio
.NET Core CLI
From Solution Explorer, right-click on the project > Add > New Scaffolded Item.
From the left pane of the Add Scaffold dialog, select Identity > ADD.
In the ADD Identity dialog, select the options you want.
Select your existing layout page, or your layout file will be overwritten with incorrect markup. For
example ~/Pages/Shared/_Layout.cshtml for Razor Pages ~/Views/Shared/_Layout.cshtml for MVC
projects
Select the + button to create a new Data context class.
Select ADD.
Agregue las siguientes llamadas resaltadas a la Startup clase:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
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)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAuthentication();
app.UseMvc();
}
}

UseHsts is recommended but not required. See HTTP Strict Transport Security Protocol for more information.
Requiere que el código de base de datos de identidad generado migraciones de Entity Framework Core. Cree una
migración y actualice la base de datos. Por ejemplo, ejecute los siguientes comandos:
Visual Studio
CLI de .NET Core
En Visual Studio consola de administrador de paquetes:

Add-Migration CreateIdentitySchema
Update-Database

El parámetro de nombre de "CreateIdentitySchema" para el Add-Migration comando es arbitrario.


"CreateIdentitySchema" Describe la migración.

Identidad de scaffolding en un proyecto de Razor sin autorización


existente
Run the Identity scaffolder:
Visual Studio
.NET Core CLI
From Solution Explorer, right-click on the project > Add > New Scaffolded Item.
From the left pane of the Add Scaffold dialog, select Identity > ADD.
In the ADD Identity dialog, select the options you want.
Select your existing layout page, or your layout file will be overwritten with incorrect markup. For
example ~/Pages/Shared/_Layout.cshtml for Razor Pages ~/Views/Shared/_Layout.cshtml for MVC
projects
Select the + button to create a new Data context class.
Select ADD.
Identidad se ha configurado en Areas/Identity/IdentityHostingStartup.cs. Para obtener más información, consulte
IHostingStartup.
Las migraciones, UseAuthentication y diseño
Requiere que el código de base de datos de identidad generado migraciones de Entity Framework Core. Cree una
migración y actualice la base de datos. Por ejemplo, ejecute los siguientes comandos:
Visual Studio
CLI de .NET Core
En Visual Studio consola de administrador de paquetes:

Add-Migration CreateIdentitySchema
Update-Database

El parámetro de nombre de "CreateIdentitySchema" para el Add-Migration comando es arbitrario.


"CreateIdentitySchema" Describe la migración.

En el Configure método de la Startup clase, llame a UseAuthentication después UseStaticFiles :

public class Startup


{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}

public IConfiguration Configuration { get; }

public void ConfigureServices(IServiceCollection services)


{
services.AddMvc();
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)


{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAuthentication();

app.UseMvc();
}
}

UseHsts is recommended but not required. See HTTP Strict Transport Security Protocol for more information.
Cambios de diseño
Opcional: Agregar el inicio de sesión parcial ( _LoginPartial ) para el archivo de diseño:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - RazorNoAuth8</title>

<environment include="Development">
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
<link rel="stylesheet" href="~/css/site.css" />
</environment>
<environment exclude="Development">
<link rel="stylesheet" href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.min.css"
asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-
value="absolute" />
<link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" />
</environment>
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-
collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a asp-page="/Index" class="navbar-brand">RazorNoAuth8</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a asp-page="/Index">Home</a></li>
<li><a asp-page="/About">About</a></li>
<li><a asp-page="/Contact">Contact</a></li>
</ul>
<partial name="_LoginPartial" />
</div>
</div>
</nav>

<partial name="_CookieConsentPartial" />

<div class="container body-content">


@RenderBody()
<hr />
<footer>
<p>&copy; 2018 - RazorNoAuth8</p>
</footer>
</div>

<environment include="Development">
<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
</environment>
<environment exclude="Development">
<script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-3.3.1.min.js"
asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
asp-fallback-test="window.jQuery"
crossorigin="anonymous"
integrity="sha384-K+ctZQ+LL8q6tP7I94W+qzQsfRV2a+AfHIi9k8z8l9ggpc8X+Ytst4yBo/hH+8Fk">
</script>
<script src="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/bootstrap.min.js"
asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.min.js"
asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal"
crossorigin="anonymous"
integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa">
</script>
</script>
<script src="~/js/site.min.js" asp-append-version="true"></script>
</environment>

@RenderSection("Scripts", required: false)


</body>
</html>

Identidad de scaffolding en un proyecto de Razor con autorización


Ejecute al scaffolder de identidad:
Visual Studio
CLI de .NET Core
De el Explorador de soluciones, haga doble clic en el proyecto > agregar > nuevo elemento de
scaffolding.
En el panel izquierdo de la agregar scaffolding cuadro de diálogo, seleccione identidad > agregar.
En el Agregar identidad cuadro de diálogo, seleccione las opciones que desee.
Seleccione la página de diseño existente, o se sobrescribirá el archivo de diseño con formato incorrecto.
Cuando se selecciona un archivo _Layout.cshtml existente, es no sobrescribe.
Por ejemplo ~/Pages/Shared/_Layout.cshtml para las páginas de Razor ~/Views/Shared/_Layout.cshtml para los
proyectos MVC
Para usar el contexto de datos existente, seleccione al menos un archivo para invalidar. Debe seleccionar al
menos un archivo para agregar el contexto de datos.
Seleccione la clase de contexto de datos.
Seleccione agregar.
Para crear un nuevo contexto de usuario y posiblemente crear una clase de usuario personalizado de identidad:
Seleccione el + botón para crear un nuevo clase de contexto de datos.
Seleccione agregar.
Nota: Si va a crear un nuevo contexto de usuario, no tienes que seleccionar un archivo para invalidar.
Algunas opciones de identidad se configuran en Areas/Identity/IdentityHostingStartup.cs. Para obtener más
información, consulte IHostingStartup.

Identidad de scaffolding en un proyecto MVC sin autorización


existente
Run the Identity scaffolder:
Visual Studio
.NET Core CLI
From Solution Explorer, right-click on the project > Add > New Scaffolded Item.
From the left pane of the Add Scaffold dialog, select Identity > ADD.
In the ADD Identity dialog, select the options you want.
Select your existing layout page, or your layout file will be overwritten with incorrect markup. For
example ~/Pages/Shared/_Layout.cshtml for Razor Pages ~/Views/Shared/_Layout.cshtml for MVC
projects
Select the + button to create a new Data context class.
Select ADD.
Opcional: Agregar el inicio de sesión parcial ( _LoginPartial ) a la Views/Shared/_Layout.cshtml archivo:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - MvcNoAuth3</title>

<environment include="Development">
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
<link rel="stylesheet" href="~/css/site.css" />
</environment>
<environment exclude="Development">
<link rel="stylesheet" href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.min.css"
asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-
value="absolute" />
<link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" />
</environment>
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-
collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a asp-area="" asp-controller="Home" asp-action="Index" class="navbar-brand">MvcNoAuth3</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a asp-area="" asp-controller="Home" asp-action="Index">Home</a></li>
<li><a asp-area="" asp-controller="Home" asp-action="About">About</a></li>
<li><a asp-area="" asp-controller="Home" asp-action="Contact">Contact</a></li>
</ul>
<partial name="_LoginPartial" />
</div>
</div>
</nav>

<partial name="_CookieConsentPartial" />

<div class="container body-content">


@RenderBody()
<hr />
<footer>
<p>&copy; 2018 - MvcNoAuth3</p>
</footer>
</div>

<environment include="Development">
<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
</environment>
<environment exclude="Development">
<script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-3.3.1.min.js"
asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
asp-fallback-test="window.jQuery"
crossorigin="anonymous"
integrity="sha384-K+ctZQ+LL8q6tP7I94W+qzQsfRV2a+AfHIi9k8z8l9ggpc8X+Ytst4yBo/hH+8Fk">
</script>
<script src="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/bootstrap.min.js"
<script src="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/bootstrap.min.js"
asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.min.js"
asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal"
crossorigin="anonymous"
integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa">
</script>
<script src="~/js/site.min.js" asp-append-version="true"></script>
</environment>

@RenderSection("Scripts", required: false)


</body>
</html>

Mover el Pages/Shared/_LoginPartial.cshtml del archivo a Views/Shared/_LoginPartial.cshtml


Identidad se ha configurado en Areas/Identity/IdentityHostingStartup.cs. Para obtener más información, consulte
IHostingStartup.
Requiere que el código de base de datos de identidad generado migraciones de Entity Framework Core. Cree una
migración y actualice la base de datos. Por ejemplo, ejecute los siguientes comandos:
Visual Studio
CLI de .NET Core
En Visual Studio consola de administrador de paquetes:

Add-Migration CreateIdentitySchema
Update-Database

El parámetro de nombre de "CreateIdentitySchema" para el Add-Migration comando es arbitrario.


"CreateIdentitySchema" Describe la migración.

Llame a UseAuthentication después UseStaticFiles :

public class Startup


{

public void ConfigureServices(IServiceCollection services)


{
services.AddMvc();
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)


{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAuthentication();
app.UseMvcWithDefaultRoute();
}
}

UseHsts is recommended but not required. See HTTP Strict Transport Security Protocol for more information.
Identidad de scaffolding en un proyecto MVC con autorización
Ejecute al scaffolder de identidad:
Visual Studio
CLI de .NET Core
De el Explorador de soluciones, haga doble clic en el proyecto > agregar > nuevo elemento de
scaffolding.
En el panel izquierdo de la agregar scaffolding cuadro de diálogo, seleccione identidad > agregar.
En el Agregar identidad cuadro de diálogo, seleccione las opciones que desee.
Seleccione la página de diseño existente, o se sobrescribirá el archivo de diseño con formato incorrecto.
Cuando se selecciona un archivo _Layout.cshtml existente, es no sobrescribe.
Por ejemplo ~/Pages/Shared/_Layout.cshtml para las páginas de Razor ~/Views/Shared/_Layout.cshtml para los
proyectos MVC
Para usar el contexto de datos existente, seleccione al menos un archivo para invalidar. Debe seleccionar al
menos un archivo para agregar el contexto de datos.
Seleccione la clase de contexto de datos.
Seleccione agregar.
Para crear un nuevo contexto de usuario y posiblemente crear una clase de usuario personalizado de identidad:
Seleccione el + botón para crear un nuevo clase de contexto de datos.
Seleccione agregar.
Nota: Si va a crear un nuevo contexto de usuario, no tienes que seleccionar un archivo para invalidar.
Eliminar el páginas/Shared carpeta y los archivos de esa carpeta.

Crear origen de la interfaz de usuario de identidad completa


Para mantener el control completo de la interfaz de usuario de identidad, ejecute el scaffolder de identidad y
seleccione invalidar todos los archivos.
El código resaltado siguiente muestra los cambios para reemplazar el valor predeterminado de interfaz de usuario
de identidad con la identidad en una aplicación web de ASP.NET Core 2.1. Puede hacer esto para tener control
total de la interfaz de usuario de identidad.
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});

services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));

services.AddIdentity<IdentityUser, IdentityRole>()
// services.AddDefaultIdentity<IdentityUser>()
.AddEntityFrameworkStores<ApplicationDbContext>();

services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
.AddRazorPagesOptions(options =>
{
options.AllowAreas = true;
options.Conventions.AuthorizeAreaFolder("Identity", "/Account/Manage");
options.Conventions.AuthorizeAreaPage("Identity", "/Account/Logout");
});

services.ConfigureApplicationCookie(options =>
{
options.LoginPath = $"/Identity/Account/Login";
options.LogoutPath = $"/Identity/Account/Logout";
options.AccessDeniedPath = $"/Identity/Account/AccessDenied";
});

// using Microsoft.AspNetCore.Identity.UI.Services;
services.AddSingleton<IEmailSender, EmailSender>();
}

Se reemplaza el valor predeterminado de identidad en el código siguiente:

services.AddIdentity<IdentityUser, IdentityRole>()
// services.AddDefaultIdentity<IdentityUser>()
.AddEntityFrameworkStores<ApplicationDbContext>();

El siguiente el código establece la LoginPath, LogoutPath, y AccessDeniedPath:

services.ConfigureApplicationCookie(options =>
{
options.LoginPath = $"/Identity/Account/Login";
options.LogoutPath = $"/Identity/Account/Logout";
options.AccessDeniedPath = $"/Identity/Account/AccessDenied";
});

Registrar un IEmailSender implementación, por ejemplo:

// using Microsoft.AspNetCore.Identity.UI.Services;
services.AddSingleton<IEmailSender, EmailSender>();
Agregar, descargar y eliminar datos de usuario
personalizada para la identidad en un proyecto de
ASP.NET Core
22/06/2018 • 10 minutes to read • Edit Online

Por Rick Anderson


Este artículo se muestra cómo:
Agregar datos de usuario personalizado a una aplicación web de ASP.NET Core.
Decorar el modelo de datos de usuario personalizada con el PersonalData atributo para que esté disponible
automáticamente para su eliminación y la descarga. Hacer que los datos que se puede descargar y eliminar
ayuda a cumplir GDPR requisitos.
Se crea el proyecto de ejemplo desde una aplicación web de las páginas de Razor, pero las instrucciones son
similares para una aplicación web de MVC de ASP.NET Core.
Vea o descargue el código de ejemplo (cómo descargarlo)

Requisitos previos
.NET Core 2.1 SDK or later

Creación de una aplicación web de Razor


Visual Studio
CLI de .NET Core
En el menú Archivo de Visual Studio, seleccione Nuevo > Proyecto. Denomine el proyecto WebApp1 si
desea coincida con el espacio de nombres de la Descargar ejemplo código.
Seleccione aplicación Web de ASP.NET Core > Aceptar
Seleccione ASP.NET Core 2.1 en la lista desplegable
Seleccione aplicación Web > Aceptar
Compile y ejecute el proyecto.

Ejecute al scaffolder de identidad


Visual Studio
CLI de .NET Core
De el Explorador de soluciones, haga doble clic en el proyecto > agregar > nuevo elemento de
scaffolding.
En el panel izquierdo de la agregar scaffolding cuadro de diálogo, seleccione identidad > agregar.
En el Agregar identidad cuadro de diálogo, las siguientes opciones:
Seleccione el archivo de diseño existente ~/Pages/Shared/_Layout.cshtml
Seleccione los archivos siguientes para reemplazar:
Cuenta/Register
Cuenta/Administrar/índice
Seleccione el + botón para crear un nuevo clase de contexto de datos. Acepte el tipo
(WebApp1.Models.WebApp1Context si denominado el proyecto WebApp1).
Seleccione el + botón para crear un nuevo User (clase). Acepte el tipo (WebApp1User si denominado
el proyecto WebApp1) > agregar.
Seleccione agregar.
Siga las instrucciones UseAuthentication, migraciones y diseño para realizar los pasos siguientes:
Cree una migración y actualice la base de datos.
Agregue UseAuthentication a Startup.Configure .
Agregar <partial name="_LoginPartial" /> para el archivo de diseño.
Pruebe la aplicación:
Registrar un usuario
Seleccione el nuevo nombre de usuario ( junto a la Logout vínculo). Deberá expandir la ventana o
seleccione el icono de la barra de navegación para mostrar el nombre de usuario y otros vínculos.
Seleccione el datos personales ficha.
Seleccione el descargar botón y examinar el PersonalData.json archivo.
Prueba la eliminar botón, lo que elimina el inicio de sesión de usuario.

Agregar datos de usuario personalizado a la base de datos de identidad


Actualización de la IdentityUser deriva la clase con propiedades personalizadas. Si con el nombre de su proyecto
WebApp1, el archivo se denomina Areas/Identity/Data/WebApp1User.cs. Actualice el archivo con el código
siguiente:

using Microsoft.AspNetCore.Identity;
using System;

namespace WebApp1.Areas.Identity.Data
{
public class WebApp1User : IdentityUser
{
[PersonalData]
public string Name { get; set; }
[PersonalData]
public DateTime DOB { get; set; }
}
}

Propiedades decorada con el PersonalData atributo son:


Cuando elimina el Areas/Identity/Pages/Account/Manage/DeletePersonalData.cshtml Razor página llama
UserManager.Delete .
Incluido en los datos descargados por el
Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtml página Razor.
Actualizar la página Account/Manage/Index.cshtml
Actualización de la InputModel en Areas/Identity/Pages/Account/Manage/Index.cshtml.cs aparece resaltado este
código con lo siguiente:

public partial class IndexModel : PageModel


{
private readonly UserManager<WebApp1User> _userManager;
private readonly SignInManager<WebApp1User> _signInManager;
private readonly IEmailSender _emailSender;
public IndexModel(
UserManager<WebApp1User> userManager,
SignInManager<WebApp1User> signInManager,
IEmailSender emailSender)
{
_userManager = userManager;
_signInManager = signInManager;
_emailSender = emailSender;
}

public string Username { get; set; }

public bool IsEmailConfirmed { get; set; }

[TempData]
public string StatusMessage { get; set; }
[BindProperty]
public InputModel Input { get; set; }

public class InputModel


{
[Required]
[DataType(DataType.Text)]
[Display(Name = "Full name")]
public string Name { get; set; }

[Required]
[Display(Name = "Birth Date")]
[DataType(DataType.Date)]
public DateTime DOB { get; set; }

[Required]
[EmailAddress]
public string Email { get; set; }

[Phone]
[Display(Name = "Phone number")]
public string PhoneNumber { get; set; }
}

public async Task<IActionResult> OnGetAsync()


{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}

var userName = await _userManager.GetUserNameAsync(user);


var email = await _userManager.GetEmailAsync(user);
var phoneNumber = await _userManager.GetPhoneNumberAsync(user);

Username = userName;

Input = new InputModel


{
Name = user.Name,
DOB = user.DOB,
Email = email,
PhoneNumber = phoneNumber
};

IsEmailConfirmed = await _userManager.IsEmailConfirmedAsync(user);

return Page();
}

public async Task<IActionResult> OnPostAsync()


{
if (!ModelState.IsValid)
{
return Page();
}

var user = await _userManager.GetUserAsync(User);


if (user == null)
{
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}

if (Input.Name != user.Name)
{
user.Name = Input.Name;
}

if (Input.DOB != user.DOB)
{
user.DOB = Input.DOB;
}

var email = await _userManager.GetEmailAsync(user);


if (Input.Email != email)
{
var setEmailResult = await _userManager.SetEmailAsync(user, Input.Email);
if (!setEmailResult.Succeeded)
{
var userId = await _userManager.GetUserIdAsync(user);
throw new InvalidOperationException($"Unexpected error occurred setting email for user with ID
'{userId}'.");
}
}

var phoneNumber = await _userManager.GetPhoneNumberAsync(user);


if (Input.PhoneNumber != phoneNumber)
{
var setPhoneResult = await _userManager.SetPhoneNumberAsync(user, Input.PhoneNumber);
if (!setPhoneResult.Succeeded)
{
var userId = await _userManager.GetUserIdAsync(user);
throw new InvalidOperationException($"Unexpected error occurred setting phone number for user
with ID '{userId}'.");
}
}

await _userManager.UpdateAsync(user);

await _signInManager.RefreshSignInAsync(user);
StatusMessage = "Your profile has been updated";
return RedirectToPage();
}

public async Task<IActionResult> OnPostSendVerificationEmailAsync()


{
if (!ModelState.IsValid)
{
return Page();
}

var user = await _userManager.GetUserAsync(User);


if (user == null)
{
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}

var userId = await _userManager.GetUserIdAsync(user);


var email = await _userManager.GetEmailAsync(user);
var email = await _userManager.GetEmailAsync(user);
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
var callbackUrl = Url.Page(
"/Account/ConfirmEmail",
pageHandler: null,
values: new { userId = userId, code = code },
protocol: Request.Scheme);
await _emailSender.SendEmailAsync(
email,
"Confirm your email",
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking
here</a>.");

StatusMessage = "Verification email sent. Please check your email.";


return RedirectToPage();
}
}

Actualización de la Areas/Identity/Pages/Account/Manage/Index.cshtml con el siguiente marcado resaltado:


@page
@model IndexModel
@{
ViewData["Title"] = "Profile";
}

<h4>@ViewData["Title"]</h4>
@Html.Partial("_StatusMessage", Model.StatusMessage)
<div class="row">
<div class="col-md-6">
<form id="profile-form" method="post">
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="Username"></label>
<input asp-for="Username" class="form-control" disabled />
</div>
<div class="form-group">
<label asp-for="Input.Email"></label>
@if (Model.IsEmailConfirmed)
{
<div class="input-group">
<input asp-for="Input.Email" class="form-control" />
<span class="input-group-addon" aria-hidden="true"><span class="glyphicon glyphicon-ok
text-success"></span></span>
</div>
}
else
{
<input asp-for="Input.Email" class="form-control" />
<button id="email-verification" type="submit" asp-page-handler="SendVerificationEmail"
class="btn btn-link">Send verification email</button>
}
<span asp-validation-for="Input.Email" class="text-danger"></span>
</div>
<div class="form-group">
<div class="form-group">
<label asp-for="Input.Name"></label>
<input asp-for="Input.Name" class="form-control" />
</div>
<div class="form-group">
<label asp-for="Input.DOB"></label>
<input asp-for="Input.DOB" class="form-control" />
</div>
<label asp-for="Input.PhoneNumber"></label>
<input asp-for="Input.PhoneNumber" class="form-control" />
<span asp-validation-for="Input.PhoneNumber" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-default">Save</button>
</form>
</div>
</div>

@section Scripts {
<partial name="_ValidationScriptsPartial" />
}

Actualizar la página Account/Register.cshtml


Actualización de la InputModel en Areas/Identity/Pages/Account/Register.cshtml.cs aparece resaltado este código
con lo siguiente:

[BindProperty]
public InputModel Input { get; set; }

public string ReturnUrl { get; set; }


public class InputModel
{
[Required]
[DataType(DataType.Text)]
[Display(Name = "Full name")]
public string Name { get; set; }

[Required]
[Display(Name = "Birth Date")]
[DataType(DataType.Date)]
public DateTime DOB { get; set; }

[Required]
[EmailAddress]
[Display(Name = "Email")]
public string Email { get; set; }

[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.",
MinimumLength = 6)]
[DataType(DataType.Password)]
[Display(Name = "Password")]
public string Password { get; set; }

[DataType(DataType.Password)]
[Display(Name = "Confirm password")]
[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
public string ConfirmPassword { get; set; }
}

public async Task<IActionResult> OnPostAsync(string returnUrl = null)


{
returnUrl = returnUrl ?? Url.Content("~/");
if (ModelState.IsValid)
{
var user = new WebApp1User {
UserName = Input.Email,
Email = Input.Email,
Name = Input.Name,
DOB = Input.DOB
};
var result = await _userManager.CreateAsync(user, Input.Password);
if (result.Succeeded)
{
_logger.LogInformation("User created a new account with password.");

var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);


var callbackUrl = Url.Page(
"/Account/ConfirmEmail",
pageHandler: null,
values: new { userId = user.Id, code = code },
protocol: Request.Scheme);

await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",


$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking
here</a>.");

await _signInManager.SignInAsync(user, isPersistent: false);


return LocalRedirect(returnUrl);
}
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
}

// If we got this far, something failed, redisplay form


return Page();
}
}

Actualización de la Areas/Identity/Pages/Account/Register.cshtml con el siguiente marcado resaltado:

@page
@model RegisterModel
@{
ViewData["Title"] = "Register";
}

<h2>@ViewData["Title"]</h2>

<div class="row">
<div class="col-md-4">
<form asp-route-returnUrl="@Model.ReturnUrl" method="post">
<h4>Create a new account.</h4>
<hr />
<div asp-validation-summary="All" class="text-danger"></div>

<div class="form-group">
<label asp-for="Input.Name"></label>
<input asp-for="Input.Name" class="form-control" />
<span asp-validation-for="Input.Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Input.DOB"></label>
<input asp-for="Input.DOB" class="form-control" />
<span asp-validation-for="Input.DOB" class="text-danger"></span>
</div>

<div class="form-group">
<label asp-for="Input.Name"></label>
<input asp-for="Input.Name" class="form-control" />
<span asp-validation-for="Input.Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Input.DOB"></label>
<input asp-for="Input.DOB" class="form-control" />
<span asp-validation-for="Input.DOB" class="text-danger"></span>
</div>

<div class="form-group">
<label asp-for="Input.Email"></label>
<input asp-for="Input.Email" class="form-control" />
<span asp-validation-for="Input.Email" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Input.Password"></label>
<input asp-for="Input.Password" class="form-control" />
<span asp-validation-for="Input.Password" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Input.ConfirmPassword"></label>
<input asp-for="Input.ConfirmPassword" class="form-control" />
<span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-default">Register</button>
</form>
</div>
</div>

@section Scripts {
<partial name="_ValidationScriptsPartial" />
}

Compile el proyecto.
Agregar una migración de los datos de usuario personalizada
Visual Studio
CLI de .NET Core
En Visual Studio consola de administrador de paquetes:

Add-Migration CustomUserData
Update-Database

Prueba de crear, ver, descargar, eliminar datos de usuario personalizada


Pruebe la aplicación:
Registrar un nuevo usuario.
Ver los datos de usuario personalizada en el /Identity/Account/Manage página.
Descargar y ver los datos personales de los usuarios de la /Identity/Account/Manage/PersonalData página.
Opciones de autenticación de sistemas operativos de
la Comunidad de ASP.NET Core
22/06/2018 • 2 minutes to read • Edit Online

Esta página contiene las opciones de autenticación proporcionado por la Comunidad de código abierto de
ASP.NET Core. Esta página se actualiza periódicamente como nuevos proveedores estén disponibles.

Proveedores de autenticación de sistemas operativos


La siguiente lista está ordenada alfabéticamente.

NOMBRE DESCRIPCIÓN

AspNet.Security.OpenIdConnect.Server (ASOS) ASOS es un bajo nivel, el primer protocolo OpenID Connect


server marco de ASP.NET Core y OWIN/Katana.

Cierge Cierge es un servidor de OpenID Connect que controla el


registro de usuario, inicio de sesión, perfiles, administración y
los inicios de sesión sociales.

Servidor Gluu Enterprise esté listo, abra el software de código fuente para
identidad, tener acceso a la administración de índices (IAM) y
el inicio de sesión único (SSO). Para obtener más información,
consulte el documentación del producto Gluu.

IdentityServer IdentityServer es un marco de OpenID Connect y OAuth 2.0


para ASP.NET Core, oficialmente certificadas por la base de
OpenID y en la regulación de la base. NET. Para obtener más
información, consulte Bienvenido a IdentityServer4
(documentación).

OpenIddict OpenIddict es un servidor de OpenID Connect de fácil de usar


para ASP.NET Core.

Para agregar un proveedor, editar esta página.


Configurar la identidad de ASP.NET Core
22/06/2018 • 12 minutes to read • Edit Online

Identidad de ASP.NET Core utiliza la configuración predeterminada para la configuración como directiva de
contraseñas, el tiempo de bloqueo y la configuración de cookies. Esta configuración puede invalidarse en la
aplicación Startup clase.

Opciones de identidad
El IdentityOptions clase representa las opciones que pueden usarse para configurar el sistema de identidades.
Identidad basada en notificaciones
IdentityOptions.ClaimsIdentity especifica la ClaimsIdentityOptions con las propiedades mostradas en la tabla.

PROPERTY DESCRIPCIÓN DEFAULT

roleClaimType Obtiene o establece el tipo de ClaimTypes.Role


notificación utilizado para una
notificación de rol.

SecurityStampClaimType Obtiene o establece el tipo de AspNet.Identity.SecurityStamp


notificación utilizado para la notificación
de marca de seguridad.

UserIdClaimType Obtiene o establece el tipo de ClaimTypes.NameIdentifier


notificación utilizado para la notificación
de identificador de usuario.

UserNameClaimType Obtiene o establece el tipo de ClaimTypes.Name


notificación utilizado para la notificación
de nombre de usuario.

Bloqueo
Impide al usuario durante un período de tiempo después de un determinado número de intentos de acceso
erróneos (valor predeterminado: bloqueo de 5 minutos después de 5 intentos de acceso). Una autenticación
correcta restablece el número de intentos de acceso erróneos y restablece el reloj.
En el ejemplo siguiente se muestra los valores predeterminados:

services.AddIdentity<ApplicationUser, IdentityRole>(options =>


{
// Lockout settings
options.Lockout.AllowedForNewUsers = true;
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
options.Lockout.MaxFailedAccessAttempts = 5;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();

Confirme que PasswordSignInAsync establece lockoutOnFailure a true :


var result = await _signInManager.PasswordSignInAsync(
Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: true);

IdentityOptions.Lockout especifica la LockoutOptions con las propiedades mostradas en la tabla.

PROPERTY DESCRIPCIÓN DEFAULT

AllowedForNewUsers Determina si un usuario nuevo puede true


bloquearse.

DefaultLockoutTimeSpan La cantidad de tiempo que un usuario 5 minutos


está bloqueado cuando se produce un
bloqueo.

MaxFailedAccessAttempts El número de intentos de acceso 5


erróneos hasta que se bloquea un
usuario, si está habilitado el bloqueo.

Contraseña
De forma predeterminada, identidad requiere que las contraseñas contengan un carácter en mayúsculas,
caracteres en minúsculas, un dígito y un carácter no alfanumérico. Las contraseñas deben tener al menos seis
caracteres. Opciones de contraseña puede cambiarse en Startup.ConfigureServices .
ASP.NET Core 2.x
ASP.NET Core 1.x
Núcleo de ASP.NET 2.0 agregado la RequiredUniqueChars propiedad. De lo contrario, las opciones son los
mismos que ASP.NET Core 1.x.

services.AddIdentity<ApplicationUser, IdentityRole>(options =>


{
// Password settings
options.Password.RequireDigit = true;
options.Password.RequiredLength = 8;
options.Password.RequiredUniqueChars = 2;
options.Password.RequireLowercase = true;
options.Password.RequireNonAlphanumeric = true;
options.Password.RequireUppercase = true;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();

IdentityOptions.Password especifica la opciones de contraseña con las propiedades mostradas en la tabla.

PROPERTY DESCRIPCIÓN DEFAULT

RequireDigit Requiere un número entre 0-9 en la true


contraseña.

RequiredLength La longitud mínima de la contraseña. 6

RequiredUniqueChars Solo se aplica a ASP.NET Core 2.0 o 1


posterior.

Requiere el número de caracteres


distintos de la contraseña.
PROPERTY DESCRIPCIÓN DEFAULT

RequireLowercase Requiere un carácter en minúscula en la true


contraseña.

RequireNonAlphanumeric Requiere un carácter que no sean true


alfanuméricos en la contraseña.

RequireUppercase Requiere un carácter en mayúsculas en true


la contraseña.

inicio de sesión

services.AddIdentity<ApplicationUser, IdentityRole>(options =>


{
// Signin settings
options.SignIn.RequireConfirmedEmail = true;
options.SignIn.RequireConfirmedPhoneNumber = false;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();

IdentityOptions.SignIn especifica la SignInOptions con las propiedades mostradas en la tabla.

PROPERTY DESCRIPCIÓN DEFAULT

RequireConfirmedEmail Requiere un correo electrónico false


confirmado para iniciar sesión en.

RequireConfirmedPhoneNumber Requiere un número de teléfono false


confirmada iniciar sesión en.

tokens
IdentityOptions.Tokens especifica la TokenOptions con las propiedades mostradas en la tabla.

PROPERTY DESCRIPCIÓN

AuthenticatorTokenProvider Obtiene o establece el AuthenticatorTokenProvider


utilizado para validar los inicios de sesión de dos fases con un
autenticador.

ChangeEmailTokenProvider Obtiene o establece el ChangeEmailTokenProvider usado


para generar tokens que se usan en correo electrónico de
confirmación del cambio de correo electrónico.

ChangePhoneNumberTokenProvider Obtiene o establece el ChangePhoneNumberTokenProvider


usado para generar tokens que se usan al cambiar los
números de teléfono.

EmailConfirmationTokenProvider Obtiene o establece el proveedor de tokens que se usa para


generar tokens que se usan en los correos electrónicos de
confirmación de cuenta.

PasswordResetTokenProvider Obtiene o establece la IUserTwoFactorTokenProvider usado


para generar tokens que se usan en los correos electrónicos
de restablecimiento de contraseña.
PROPERTY DESCRIPCIÓN

ProviderMap Utilizado para construir un proveedor de tokens de usuario


con la clave que se usa como el nombre del proveedor.

Usuario

services.AddIdentity<ApplicationUser, IdentityRole>(options =>


{
// User settings
options.User.RequireUniqueEmail = true;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();

IdentityOptions.User especifica la UserOptions con las propiedades mostradas en la tabla.

PROPERTY DESCRIPCIÓN DEFAULT

AllowedUserNameCharacters Caracteres permitidos en el nombre de abcdefghijklmnopqrstuvwxyz


usuario. ABCDEFGHIJKLMNOPQRSTUVWXYZ
0123456789
-.@+

RequireUniqueEmail Requiere que cada usuario tiene un false


correo electrónico único.

Configuración de cookies
Configurar la cookie de la aplicación en Startup.ConfigureServices :
ASP.NET Core 2.x
ASP.NET Core 1.x

services.ConfigureApplicationCookie(options =>
{
options.AccessDeniedPath = "/Account/AccessDenied";
options.Cookie.Name = "YourAppCookieName";
options.Cookie.HttpOnly = true;
options.ExpireTimeSpan = TimeSpan.FromMinutes(60);
options.LoginPath = "/Account/Login";
// ReturnUrlParameter requires `using Microsoft.AspNetCore.Authentication.Cookies;`
options.ReturnUrlParameter = CookieAuthenticationDefaults.ReturnUrlParameter;
options.SlidingExpiration = true;
});

CookieAuthenticationOptions tiene las siguientes propiedades:

PROPERTY DESCRIPCIÓN

AccessDeniedPath Informa al controlador que debe cambiar en una salida 403


Prohibido código de estado en un redireccionamiento 302 en
la ruta de acceso especificada.

El valor predeterminado es /Account/AccessDenied .


PROPERTY DESCRIPCIÓN

AuthenticationScheme Solo se aplica a ASP.NET Core 1.x.

El nombre lógico para un esquema de autenticación concreto.

AutomaticAuthenticate Solo se aplica a ASP.NET Core 1.x.

Cuando sea true, autenticación con cookies debe ejecutar en


cada solicitud y se intentan validar y reconstruir cualquier
entidad de seguridad serializado que creó.

AutomaticChallenge Solo se aplica a ASP.NET Core 1.x.

Si es true, el middleware de autenticación controla desafíos


automática. Si false, el middleware de autenticación sólo altera
las respuestas cuando indique explícitamente la
AuthenticationScheme .

ClaimsIssuer Obtiene o establece el emisor que se debe usar para las


notificaciones que se crean (se hereda de
AuthenticationSchemeOptions).

Cookie.Domain Dominio que se va a asociar con la cookie.

Cookie.Expiration Obtiene o establece la duración de la cookie HTTP (no la


cookie de autenticación). Esta propiedad se reemplaza por
ExpireTimeSpan. Que no debe usarse en el contexto de
CookieAuthentication.

Cookie.HttpOnly Indica si una cookie es accesible mediante script de cliente.

El valor predeterminado es true .

Cookie.Name El nombre de la cookie.

El valor predeterminado es .AspNetCore.Cookies .

Cookie.Path La ruta de acceso de la cookie.

Cookie.SameSite El SameSite atributo de la cookie.

El valor predeterminado es SameSiteMode.Lax.

Cookie.SecurePolicy El CookieSecurePolicy configuración.

El valor predeterminado es
CookieSecurePolicy.SameAsRequest.

CookieDomain Solo se aplica a ASP.NET Core 1.x.

El nombre de dominio donde se sirve la cookie.


PROPERTY DESCRIPCIÓN

CookieHttpOnly Solo se aplica a ASP.NET Core 1.x.

Una marca que indica si la cookie debe ser accesible sólo a los
servidores.

El valor predeterminado es true .

CookiePath Solo se aplica a ASP.NET Core 1.x.

Se utiliza para aislar las aplicaciones que se ejecutan en el


mismo nombre de host.

CookieSecure Solo se aplica a ASP.NET Core 1.x.

Una marca que indica si la cookie creada debe limitarse a


HTTPS ( CookieSecurePolicy.Always ), HTTP o HTTPS (
CookieSecurePolicy.None ), o el mismo protocolo que la
solicitud ( CookieSecurePolicy.SameAsRequest ).

El valor predeterminado es
CookieSecurePolicy.SameAsRequest .

CookieManager El componente utilizado para obtener las cookies de la


solicitud o para establecerlas en la respuesta.

DataProtectionProvider Si se establece, el proveedor utiliza por la


CookieAuthenticationHandler para la protección de datos.

Descripción Solo se aplica a ASP.NET Core 1.x.

Información adicional sobre el tipo de autenticación que debe


ponerse a disposición de la aplicación.

Eventos El controlador llama a métodos en el proveedor que


proporcionan al control de la aplicación en determinados
puntos donde se está produciendo el procesamiento.

EventsType Si se establece, el servicio escriba para obtener la Events


instancia en lugar de la propiedad (se hereda de
AuthenticationSchemeOptions).

ExpireTimeSpan Controla cuánto tiempo el vale de autenticación que se


almacena en la cookie se conserva válida desde el punto en
que se crea.

El valor predeterminado es 14 días.

LoginPath Cuando un usuario está autorizado, se le redirige a esta ruta


de acceso al inicio de sesión.

El valor predeterminado es /Account/Login .

LogoutPath Cuando un usuario se cerró la sesión, se le redirige a esta ruta


de acceso.

El valor predeterminado es /Account/Logout .


PROPERTY DESCRIPCIÓN

ReturnUrlParameter Determina el nombre del parámetro de cadena de consulta


que se anexa el middleware cuando un 401 no autorizado
código de estado se cambia a un redireccionamiento 302 en
la ruta de acceso de inicio de sesión.

El valor predeterminado es ReturnUrl .

SessionStore Contenedor opcional que se va a almacenar la identidad de


todas las solicitudes.

slidingExpiration Cuando sea true, se emite una cookie nueva con una nueva
hora de expiración cuando la cookie actual está en más de
medio a través de la ventana de expiración.

El valor predeterminado es true .

TicketDataFormat El TicketDataFormat se usa para proteger y desproteger la


identidad y otras propiedades que se almacenan en el valor de
cookie.
Configurar la autenticación de Windows en
ASP.NET Core
22/06/2018 • 8 minutes to read • Edit Online

Por Steve Smith y Scott Addie


Se puede configurar la autenticación de Windows para las aplicaciones ASP.NET Core hospedadas en IIS,
HTTP.sys, o WebListener.

¿Qué es la autenticación de Windows?


Autenticación de Windows se basa en el sistema operativo para autenticar a los usuarios de las aplicaciones de
ASP.NET Core. Puede utilizar la autenticación de Windows cuando el servidor se ejecuta en una red
corporativa utilizando las identidades del dominio de Active Directory o a otras cuentas de Windows para
identificar a los usuarios. Autenticación de Windows es más adecuada para entornos de intranet en el que los
usuarios, las aplicaciones cliente y los servidores web pertenecen al mismo dominio de Windows.
Obtener más información sobre la autenticación de Windows e instalarla para IIS.

Habilitar la autenticación de Windows en una aplicación de ASP.NET


Core
La plantilla de aplicación Web de Visual Studio puede configurarse para admitir la autenticación de Windows.
Usa la plantilla de aplicación de autenticación de Windows
En Visual Studio:
1. Cree una aplicación web de ASP.NET Core.
2. En la lista de plantillas, seleccione aplicación Web.
3. Seleccione el Cambiar autenticación botón y seleccione autenticación de Windows.
Ejecute la aplicación. El nombre de usuario aparece en la parte superior derecha de la aplicación.
Para el trabajo de desarrollo con IIS Express, la plantilla proporciona la configuración necesaria para utilizar la
autenticación de Windows. La sección siguiente muestra cómo configurar manualmente una aplicación de
ASP.NET Core para la autenticación de Windows.
Configuración de Visual Studio para Windows y la autenticación anónima
El proyecto de Visual Studio propiedades la página depurar ficha proporciona casillas de verificación para la
autenticación de Windows y la autenticación anónima.
Como alternativa, estas dos propiedades pueden configurarse en el launchSettings.json archivo:

{
"iisSettings": {
"windowsAuthentication": true,
"anonymousAuthentication": false,
"iisExpress": {
"applicationUrl": "http://localhost:52171/",
"sslPort": 0
}
} // additional options trimmed
}

Habilitar la autenticación de Windows con IIS


IIS usa el módulo principal de ASP.NET a las aplicaciones ASP.NET Core de host. El módulo permite la
autenticación de Windows para que fluya a IIS de forma predeterminada. Se configura la autenticación de
Windows en IIS, no la aplicación. Las secciones siguientes muestran cómo usar el Administrador de IIS para
configurar una aplicación ASP.NET Core para usar la autenticación de Windows.
Crear un nuevo sitio IIS
Especifique un nombre y una carpeta y permitir que se cree un nuevo grupo de aplicaciones.
Personalizar la autenticación
Abra el menú de autenticación para el sitio.
Deshabilite la autenticación anónima y habilitar la autenticación de Windows.

Publicar el proyecto en la carpeta del sitio IIS


Con Visual Studio o la CLI de núcleo. NET, publique la aplicación en la carpeta de destino.
Obtenga más información sobre publicar en IIS.
Inicie la aplicación para comprobar que funciona la autenticación de Windows.

Habilitar la autenticación de Windows con HTTP.sys o WebListener


ASP.NET Core 2.x
ASP.NET Core 1.x
Aunque Kestrel no admite la autenticación de Windows, puede usar HTTP.sys para admitir escenarios
hospedados por sí mismo en Windows. En el ejemplo siguiente se configura el host de la aplicación web para
usar HTTP.sys con autenticación de Windows:

public class Program


{
public static void Main(string[] args) =>
BuildWebHost(args).Run();

public static IWebHost BuildWebHost(string[] args) =>


WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.UseHttpSys(options =>
{
options.Authentication.Schemes =
AuthenticationSchemes.NTLM | AuthenticationSchemes.Negotiate;
options.Authentication.AllowAnonymous = false;
})
.Build();
}

Trabajar con la autenticación de Windows


El estado de configuración de acceso anónimo determina la manera en que la [Authorize] y [AllowAnonymous]
atributos se utilizan en la aplicación. Las dos secciones siguientes explican cómo controlar los Estados de
configuración permitidos y no permitidos de acceso anónimo.
Denegar el acceso anónimo
Cuando se habilita la autenticación de Windows y acceso anónimo está deshabilitado, el [Authorize] y
[AllowAnonymous] atributos no tienen ningún efecto. Si el sitio IIS (o servidor HTTP.sys o WebListener ) está
configurado para denegar el acceso anónimo, la solicitud nunca llega a la aplicación. Por este motivo, la
[AllowAnonymous] atributo no se aplica.

Permitir el acceso anónimo


Cuando se habilitan la autenticación de Windows y el acceso anónimo, use la [Authorize] y [AllowAnonymous]
atributos. El [Authorize] atributo permite proteger partes de la aplicación que realmente requieren
autenticación de Windows. El [AllowAnonymous] atributo invalidaciones [Authorize] atributo uso dentro de las
aplicaciones que permiten el acceso anónimo. Vea autorización sencilla para obtener detalles de uso de
atributos.
En ASP.NET Core 2.x, el [Authorize] atributo requiere una configuración adicional de Startup.cs Desafíe
solicitudes anónimas para la autenticación de Windows. La configuración recomendada varía ligeramente
según el servidor web que se va a usar.

NOTE
De forma predeterminada, los usuarios que no tienen autorización para acceder a una página se le presentará una
respuesta HTTP 403 vacía. El StatusCodePages middleware se puede configurar para proporcionar a los usuarios una
mejor experiencia de "Acceso denegado".

IIS
Si usa IIS, agregue lo siguiente a la ConfigureServices método:

// IISDefaults requires the following import:


// using Microsoft.AspNetCore.Server.IISIntegration;
services.AddAuthentication(IISDefaults.AuthenticationScheme);

HTTP.sys
Si utiliza HTTP.sys, agregue lo siguiente a la ConfigureServices método:

// HttpSysDefaults requires the following import:


// using Microsoft.AspNetCore.Server.HttpSys;
services.AddAuthentication(HttpSysDefaults.AuthenticationScheme);

Suplantación
ASP.NET Core no implementa la suplantación. Las aplicaciones se ejecutan con la identidad de aplicación para
todas las solicitudes, uso de la identidad de proceso o grupo de servidores de aplicación. Si necesita realizar
una acción en nombre del usuario de forma explícita, use WindowsIdentity.RunImpersonated . Ejecutar una sola
acción en este contexto y, a continuación, cierre el contexto.
app.Run(async (context) =>
{
try
{
var user = (WindowsIdentity)context.User.Identity;

await context.Response
.WriteAsync($"User: {user.Name}\tState: {user.ImpersonationLevel}\n");

WindowsIdentity.RunImpersonated(user.AccessToken, () =>
{
var impersonatedUser = WindowsIdentity.GetCurrent();
var message =
$"User: {impersonatedUser.Name}\tState: {impersonatedUser.ImpersonationLevel}";

var bytes = Encoding.UTF8.GetBytes(message);


context.Response.Body.Write(bytes, 0, bytes.Length);
});
}
catch (Exception e)
{
await context.Response.WriteAsync(e.ToString());
}
});

Tenga en cuenta que RunImpersonated no es compatible con operaciones asincrónicas y no deben usarse para
escenarios complejos. Por ejemplo, el ajuste de las solicitudes de todas o cadenas de middleware no es
compatible ni recomendable.
Configurar el tipo de datos de clave principal de
identidad en ASP.NET Core
22/06/2018 • 3 minutes to read • Edit Online

Identidad de ASP.NET Core le permite configurar el tipo de datos que se utiliza para representar una clave
principal. Identidad utiliza el string tipo de datos de forma predeterminada. Puede invalidar este
comportamiento.

Personalizar el tipo de datos de clave principal


1. Crear una implementación personalizada de la IdentityUser clase. Representa el tipo que se usará para
crear objetos de usuario. En el ejemplo siguiente, el valor predeterminado string tipo se reemplaza con
Guid .

namespace webapptemplate.Models
{
// Add profile data for application users by adding properties to the ApplicationUser class
public class ApplicationUser : IdentityUser<Guid>
{
}
}

2. Crear una implementación personalizada de la IdentityRole clase. Representa el tipo que se usará para crear
objetos de rol. En el ejemplo siguiente, el valor predeterminado string tipo se reemplaza con Guid .

namespace webapptemplate.Models
{
public class ApplicationRole : IdentityRole<Guid>
{
}
}

3. Cree una clase de contexto de base de datos personalizada. Hereda de la clase de contexto de base de datos
de Entity Framework usada para la identidad. El TUser y TRole argumentos hacen referencia a las clases
de usuario y el rol personalizadas creadas en el paso anterior, respectivamente. El Guid se define el tipo de
datos para la clave principal.
namespace webapptemplate.Data
{
public class ApplicationDbContext : IdentityDbContext<ApplicationUser, ApplicationRole, Guid>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}

protected override void OnModelCreating(ModelBuilder builder)


{
base.OnModelCreating(builder);
// Customize the ASP.NET Identity model and override the defaults if needed.
// For example, you can rename the ASP.NET Identity table names and more.
// Add your customizations after calling base.OnModelCreating(builder);
}
}
}

4. Registre la clase de contexto de base de datos personalizada al agregar el servicio de identidad en la clase
de inicio de la aplicación.
ASP.NET Core 2.x
ASP.NET Core 1.x
El AddEntityFrameworkStores método no aceptar un TKey argumento tal como se hacía en ASP.NET Core
1.x. Tipo de datos de la clave principal se deduce mediante el análisis de la DbContext objeto.

public void ConfigureServices(IServiceCollection services)


{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

services.AddIdentity<ApplicationUser, ApplicationRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();

// Add application services.


services.AddTransient<IEmailSender, EmailSender>();

services.AddMvc();

Probar los cambios


Tras la finalización de los cambios de configuración, la propiedad que representa la clave principal refleja el nuevo
tipo de datos. En el ejemplo siguiente se muestra cómo obtener acceso a la propiedad en un controlador MVC.

[HttpGet]
[AllowAnonymous]
public async Task<Guid> GetCurrentUserId()
{
ApplicationUser user = await _userManager.GetUserAsync(HttpContext.User);
return user.Id; // No need to cast here because user.Id is already a Guid, and not a string
}
Proveedores de almacenamiento personalizados para
ASP.NET Core Identity
22/06/2018 • 20 minutes to read • Edit Online

Por Steve Smith


Identidad de ASP.NET Core es un sistema extensible que permite crear un proveedor de almacenamiento
personalizado y conectarlo a la aplicación. En este tema se describe cómo crear un proveedor de almacenamiento
personalizado de ASP.NET Core Identity. Explica los conceptos importantes para crear su propio proveedor de
almacenamiento, pero no es un tutorial paso a paso.
Vea o descargue el ejemplo de GitHub.

Introducción
De forma predeterminada, el sistema de identidades de ASP.NET Core almacena información de usuario en una
base de datos de SQL Server con Entity Framework Core. Para muchas aplicaciones, este enfoque funciona bien.
Sin embargo, es preferible utilizar un mecanismo de persistencia diferentes o el esquema de datos. Por ejemplo:
Usa almacenamiento de tablas Azure o en otro almacén de datos.
Las tablas de base de datos tienen una estructura distinta.
Puede que desee utilizar un enfoque de acceso a datos diferentes, como Dapper.
En cada uno de estos casos, puede escribir un proveedor personalizado para su mecanismo de almacenamiento y
conecte dicho proveedor de la aplicación.
Identidad de ASP.NET Core se incluye en las plantillas de proyecto en Visual Studio con la opción de "Cuentas de
usuario individuales".
Cuando se usa la CLI de núcleo. NET, agregue -au Individual :

dotnet new mvc -au Individual


dotnet new webapi -au Individual

La arquitectura de ASP.NET Core Identity


ASP.NET Core Identity consta de las clases denominadas administradores y almacenes. Administradores de son
clases de alto nivel que un desarrollador de aplicaciones que se utiliza para realizar operaciones, como la creación
de un usuario de identidad. Almacenes de son clases de nivel inferior que especifican cómo se conservan las
entidades, como los roles y los usuarios. Almacenes de seguir la modelo de repositorio y se acoplan estrechamente
con el mecanismo de persistencia. Los administradores se desacoplan de almacenes, lo que significa que puede
reemplazar el mecanismo de persistencia sin cambiar el código de aplicación (excepto la configuración).
El diagrama siguiente muestra cómo una aplicación web interactúa con los administradores, mientras que los
almacenes interactúan con la capa de acceso a datos.
Para crear un proveedor de almacenamiento personalizado, cree el origen de datos, la capa de acceso a datos y las
clases de almacén que interactúan con este nivel de acceso de datos (los cuadros de color verde y gris en el
diagrama anterior). No es necesario personalizar los administradores o su código de aplicación que interactúa con
ellos (los cuadros azules anteriores).
Cuando se crea una nueva instancia de UserManager o RoleManager proporciona el tipo de la clase de usuario y
pasar una instancia de la clase de almacén como un argumento. Este enfoque permite conectar las clases
personalizadas en ASP.NET Core.
Volver a configurar la aplicación para que use el nuevo proveedor de almacenamiento muestra cómo crear una
instancia UserManager y RoleManager con un almacén personalizado.

Tipos de datos de almacenes de identidades de ASP.NET Core


ASP.NET Core Identity tipos de datos se detallan en las secciones siguientes:
Usuarios
Usuarios registrados de su sitio web. El IdentityUser tipo puede ampliada o se utiliza como ejemplo para su propio
tipo personalizado. No es necesario heredar de un tipo determinado para implementar su propia solución de
almacenamiento de información de identidad personalizada.
Notificaciones de usuario
Un conjunto de instrucciones (o notificaciones) acerca del usuario que representan la identidad del usuario. Puede
habilitar la expresión mayor de la identidad del usuario que se puede lograr a través de roles.
Inicios de sesión de usuario
Información sobre el proveedor de autenticación externo (por ejemplo, Facebook o una cuenta de Microsoft) que
se usará cuando un usuario de inicio de sesión. Ejemplo
Roles
Grupos de autorización para el sitio. Incluye el nombre de identificador y el rol del rol (por ejemplo, "Admin" o
"Employee"). Ejemplo

La capa de acceso a datos


En este tema se da por supuesto que está familiarizado con el mecanismo de persistencia que se va a usar y cómo
crear entidades para dicho mecanismo. En este tema no proporciona detalles acerca de cómo crear los repositorios
o clases de acceso a datos; proporciona algunas sugerencias acerca de las decisiones de diseño cuando se trabaja
con la identidad de núcleo de ASP.NET.
Tiene mucha libertad al diseñar la capa de acceso a datos para un proveedor de almacén personalizado. Basta con
crear mecanismos de persistencia para las características que desee usar en la aplicación. Por ejemplo, si no se usan
funciones en la aplicación, no es necesario crear un almacenamiento para roles o las asociaciones de rol de usuario.
La tecnología y la infraestructura existente pueden requerir una estructura que es muy diferente de la
implementación predeterminada de ASP.NET Core Identity. En la capa de acceso a datos, proporcionar la lógica
para trabajar con la estructura de su implementación de almacenamiento.
La capa de acceso a datos proporciona la lógica para guardar los datos de identidad de núcleo de ASP.NET en un
origen de datos. La capa de acceso a datos para su proveedor de almacenamiento personalizado podría incluir las
siguientes clases para almacenar la información de usuario y el rol.
Context (clase )
Encapsula la información para conectarse a su mecanismo de persistencia y ejecutar consultas. Varias clases de
datos requieren una instancia de esta clase, por lo general se proporcionan a través de inserción de dependencias.
En el ejemplo se.
Almacenamiento de información de usuario
Almacena y recupera información de usuario (por ejemplo, el hash de nombre y la contraseña de usuario). Ejemplo
Almacenamiento de rol
Almacena y recupera información de funciones (por ejemplo, el nombre de rol). Ejemplo
Almacenamiento de UserClaims
Almacena y recupera información de notificaciones de usuario (por ejemplo, el tipo de notificación y el valor).
Ejemplo
Almacenamiento de UserLogins
Almacena y recupera información de inicio de sesión de usuario (por ejemplo, un proveedor de autenticación
externo). Ejemplo
Almacenamiento de UserRole
Almacena y recupera los roles asignados a los usuarios. Ejemplo
Sugerencia: implementar solo las clases que va a usar en la aplicación.
En las clases de acceso a datos, proporcionar código para realizar operaciones de datos para el mecanismo de
persistencia. Por ejemplo, dentro de un proveedor personalizado, podría tener el código siguiente para crear un
nuevo usuario en el almacenar clase:
public async Task<IdentityResult> CreateAsync(ApplicationUser user,
CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
if (user == null) throw new ArgumentNullException(nameof(user));

return await _usersTable.CreateAsync(user);


}

La lógica de implementación para crear el usuario está en el _usersTable.CreateAsync método, se muestra a


continuación.

Personalizar la clase de usuario


Al implementar un proveedor de almacenamiento, cree una clase de usuario que es equivalente a la IdentityUser
clase.
Como mínimo, debe incluir la clase de usuario una Id y un UserName propiedad.
El IdentityUser clase define las propiedades que el UserManager llamadas al realizar operaciones solicitadas. El
tipo predeterminado de la Id propiedad es una cadena, pero se puede heredar de
IdentityUser<TKey, TUserClaim, TUserRole, TUserLogin, TUserToken> y especifique un tipo diferente. El marco de
trabajo espera que la implementación de almacenamiento para controlar las conversiones de tipo de datos.

Personalizar el almacén del usuario


Crear un UserStore clase que proporciona los métodos para todas las operaciones de datos en el usuario. Esta
clase es equivalente a la UserStore clase. En su UserStore clase, implemente IUserStore<TUser> y las interfaces
opcionales requeridas. Seleccione qué interfaces opcionales que se implementarán en función de la funcionalidad
de la aplicación.
Interfaces opcionales
IUserRoleStore /dotnet/api/microsoft.aspnetcore.identity.iuserrolestore-1
IUserClaimStore /dotnet/api/microsoft.aspnetcore.identity.iuserclaimstore-1
/Dotnet/api/microsoft.aspnetcore.identity.iuserpasswordstore-1 iuserpasswordstore.
IUserSecurityStampStore
IUserEmailStore
IPhoneNumberStore
IQueryableUserStore
IUserLoginStore
IUserTwoFactorStore
IUserLockoutStore
Las interfaces opcionales heredan de IUserStore . Puede ver un usuario de ejemplo parcialmente implementado
almacenar aquí.
Dentro de la UserStore (clase), se utilizan las clases de acceso de datos que ha creado para realizar operaciones.
Estos se pasan mediante la inserción de dependencia. Por ejemplo, en el servidor SQL Server con la
implementación Dapper, la UserStore clase tiene la CreateAsync método que usa una instancia de
DapperUsersTable para insertar un nuevo registro:
public async Task<IdentityResult> CreateAsync(ApplicationUser user)
{
string sql = "INSERT INTO dbo.CustomUser " +
"VALUES (@id, @Email, @EmailConfirmed, @PasswordHash, @UserName)";

int rows = await _connection.ExecuteAsync(sql, new { user.Id, user.Email, user.EmailConfirmed,


user.PasswordHash, user.UserName });

if(rows > 0)
{
return IdentityResult.Success;
}
return IdentityResult.Failed(new IdentityError { Description = $"Could not insert user {user.Email}." });
}

Interfaces para implementar al personalizar el almacén de usuario


IUserStore
El IUserStore<TUser> interfaz es la única interfaz que debe implementar en el almacén del usuario. Define
métodos para crear, actualizar, eliminar y recuperar los usuarios.
IUserClaimStore
El IUserClaimStore<TUser> interfaz define los métodos que se implementan para habilitar notificaciones de
usuario. Contiene métodos para agregar, quitar y recuperar notificaciones de usuario.
IUserLoginStore
El IUserLoginStore<TUser> define los métodos que se implementan para permitir a los proveedores de
autenticación externo. Contiene métodos para agregar, quitar y recuperar los inicios de sesión de usuario y un
método para recuperar un usuario basándose en la información de inicio de sesión.
IUserRoleStore
El IUserRoleStore<TUser> interfaz define los métodos que se implementan para asignar un usuario a un rol.
Contiene métodos para agregar, quitar y recuperar los roles de usuario y un método para comprobar si un
usuario está asignado a un rol.
Iuserpasswordstore.
El IUserPasswordStore<TUser> interfaz define los métodos que se implementan para almacenar contraseñas
con algoritmo hash. Contiene métodos para obtener y establecer la contraseña con hash y un método que
indica si el usuario ha establecido una contraseña.
IUserSecurityStampStore
El IUserSecurityStampStore<TUser> interfaz define los métodos que se implementan para usar una marca de
seguridad para que indica si ha cambiado la información de la cuenta del usuario. Esta marca se actualiza
cuando un usuario cambia la contraseña, o agregue o quite inicios de sesión. Contiene métodos para obtener y
establecer la marca de seguridad.
IUserTwoFactorStore
El IUserTwoFactorStore<TUser> interfaz define los métodos que se implementan para admitir la autenticación
en dos fases. Contiene métodos para obtener y establecer si está habilitada la autenticación en dos fases para
un usuario.
IUserPhoneNumberStore
El IUserPhoneNumberStore<TUser> interfaz define los métodos que implementa para almacenar números de
teléfono del usuario. Contiene métodos para obtener y establecer el número de teléfono y si se ha confirmado
el número de teléfono.
IUserEmailStore
El IUserEmailStore<TUser> interfaz define los métodos que implementa para almacenar direcciones de correo
electrónico del usuario. Contiene métodos para obtener y establecer la dirección de correo electrónico y si se ha
confirmado el correo electrónico.
IUserLockoutStore
El IUserLockoutStore<TUser> interfaz define los métodos que se implementan para almacenar información
acerca de los bloqueos de una cuenta. Contiene métodos para realizar el seguimiento de intentos de acceso
erróneos y bloqueos.
IQueryableUserStore
El IQueryableUserStore<TUser> interfaz define los miembros que implementa para proporcionar un almacén
de usuarios consultable.
Implementar sólo las interfaces que son necesarios en la aplicación. Por ejemplo:

public class UserStore : IUserStore<IdentityUser>,


IUserClaimStore<IdentityUser>,
IUserLoginStore<IdentityUser>,
IUserRoleStore<IdentityUser>,
IUserPasswordStore<IdentityUser>,
IUserSecurityStampStore<IdentityUser>
{
// interface implementations not shown
}

IdentityUserClaim, IdentityUserLogin y IdentityUserRole


El Microsoft.AspNet.Identity.EntityFramework espacio de nombres contiene las implementaciones de la
IdentityUserClaim, IdentityUserLogin, y IdentityUserRole clases. Si está usando estas características, puede crear
sus propias versiones de estas clases y definir las propiedades de la aplicación. Sin embargo, a veces resulta más
eficaz para no cargar estas entidades en memoria al realizar operaciones básicas (como agregar o quitar la
notificación del usuario). En su lugar, las clases de almacenamiento back-end pueden ejecutar estas operaciones
directamente en el origen de datos. Por ejemplo, el UserStore.GetClaimsAsync método puede llamar a la
userClaimTable.FindByUserId(user.Id) método para ejecutar una consulta en que la tabla directamente y devolver
una lista de notificaciones.

Personalizar el role (clase)


Al implementar un proveedor de almacenamiento de rol, puede crear un tipo de función personalizado. No
necesita implementar una interfaz concreta, pero debe tener un Id y normalmente tendrá un Name propiedad.
La siguiente es una clase de rol de ejemplo:

using System;

namespace CustomIdentityProviderSample.CustomProvider
{
public class ApplicationRole
{
public Guid Id { get; set; } = Guid.NewGuid();
public string Name { get; set; }
}
}

Personalizar el almacén de roles


Puede crear un RoleStore clase que proporciona los métodos para todas las operaciones de datos en los roles.
Esta clase es equivalente a la RoleStore clase. En el RoleStore (clase), implementar la IRoleStore<TRole> y,
opcionalmente, la IQueryableRoleStore<TRole> interfaz.
IRoleStore<TRole>
El IRoleStore interfaz define los métodos que se implementan en la clase de almacén de rol. Contiene métodos
para crear, actualizar, eliminar y recuperar roles.
RoleStore<TRole>
Personalizar RoleStore , cree una clase que implementa el IRoleStore interfaz.

Volver a configurar la aplicación para que use el nuevo proveedor de


almacenamiento
Una vez que ha implementado un proveedor de almacenamiento, configure la aplicación para que lo utilice. Si la
aplicación utiliza el proveedor predeterminado, reemplácelo por el proveedor personalizado.
1. Quitar el Microsoft.AspNetCore.EntityFramework.Identity paquete NuGet.
2. Si el proveedor de almacenamiento reside en un proyecto independiente o un paquete, agregue una referencia
a él.
3. Reemplace todas las referencias a Microsoft.AspNetCore.EntityFramework.Identity con el uso de una instrucción
para el espacio de nombres del proveedor de almacenamiento.
4. En el ConfigureServices (método), cambiar la AddIdentity método usar sus tipos personalizados. Puede crear
sus propios métodos de extensión para este propósito. Vea IdentityServiceCollectionExtensions para obtener un
ejemplo.
5. Si está utilizando Roles, actualice el RoleManager usar su RoleStore clase.
6. Actualizar la cadena de conexión y las credenciales para la configuración de la aplicación.
Ejemplo:

public void ConfigureServices(IServiceCollection services)


{
// Add identity types
services.AddIdentity<ApplicationUser, ApplicationRole>()
.AddDefaultTokenProviders();

// Identity Services
services.AddTransient<IUserStore<ApplicationUser>, CustomUserStore>();
services.AddTransient<IRoleStore<ApplicationRole>, CustomRoleStore>();
string connectionString = Configuration.GetConnectionString("DefaultConnection");
services.AddTransient<SqlConnection>(e => new SqlConnection(connectionString));
services.AddTransient<DapperUsersTable>();

// additional configuration
}

Referencias
Proveedores de almacenamiento personalizado para identidades de ASP.NET
ASP.NET Core Identity -este repositorio incluye vínculos a la Comunidad mantenida los proveedores de
almacenes.
Autenticación con Facebook, Google y proveedores
externos en ASP.NET Core
21/06/2018 • 7 minutes to read • Edit Online

Por Valeriy Novytskyy y Rick Anderson


En este tutorial se muestra cómo crear una aplicación de ASP.NET Core 2.x que permita a los usuarios iniciar
sesión mediante OAuth 2.0 con credenciales de proveedores de autenticación externos.
En las siguientes secciones se tratan los proveedores Facebook, Twitter, Google y Microsoft. Hay disponibles
otros proveedores en paquetes de terceros como AspNet.Security.OAuth.Providers y
AspNet.Security.OpenId.Providers.

Permitir que los usuarios inicien sesión con sus credenciales es práctico para los usuarios y transfiere muchas
de las complejidades existentes en la administración del proceso de inicio de sesión a un tercero. Para ver
ejemplos de cómo los inicios de sesión de las redes sociales pueden controlar las conversiones del tráfico y de
clientes, vea los casos prácticos de Facebook y Twitter.
Nota: Los paquetes que se presentan aquí sintetizan en gran parte la complejidad del flujo de autenticación de
OAuth, pero conocer los detalles puede ser necesario a la hora de solucionar problemas. Hay disponibles
numerosos recursos; por ejemplo, vea An Introduction to OAuth 2 (Introducción a OAuth 2) o Understanding
OAuth2 (Descripción de OAuth2). Algunos problemas se pueden resolver examinando el código fuente de
ASP.NET Core de los paquetes de los proveedores.

Crear un proyecto de ASP.NET Core


En Visual Studio 2017, cree un proyecto en la página de inicio o a través de Archivo > Nuevo >
Proyecto.
Seleccione la plantilla Aplicación web de ASP.NET Core disponible en la categoría Visual C# > .NET
Core:
Pulse Aplicación web y compruebe que la opción Autenticación está establecida en Cuentas de usuario
individuales:

Nota: Este tutorial se aplica a la versión 2.0 del SDK de ASP.NET Core, que se puede seleccionar en la parte
superior del asistente.

Aplicación de migraciones
Ejecute la aplicación y seleccione el vínculo Iniciar sesión.
Seleccione el vínculo Register as a new user Registrarse como usuario nuevo).
Escriba el correo electrónico y la contraseña de la cuenta nueva y, luego, seleccione Registrarse.
Siga estas instrucciones para aplicar las migraciones.
Requerir SSL
OAuth 2.0 requiere el uso de SSL para la autenticación mediante el protocolo HTTPS.
Nota: Los proyectos creados con plantillas de proyecto de aplicación web o API Web de ASP.NET Core 2.x se
configuran automáticamente para habilitar SSL e iniciarse con una dirección URL https si la opción Cuentas
de usuario individuales estaba seleccionada en el cuadro de diálogo Cambiar autenticación del asistente
de proyectos, como se muestra arriba.
Establezca que SSL sea obligatorio en el sitio con los pasos descritos en el tema Exigir SSL en una
aplicación ASP.NET Core.

Uso de SecretManager para almacenar los tokens asignados por los


proveedores de inicio de sesión
Los proveedores de inicio de sesión de las redes sociales asignan tokens de Id. de aplicación y de secreto de
aplicación durante el proceso de registro (la nomenclatura exacta varía según el proveedor).
Estos valores son el nombre de usuario y la contraseña que usa la aplicación para obtener acceso a la API.
Conforman los "secretos" que se pueden vincular a la configuración de la aplicación con la ayuda del
Administrador de secretos, en vez de almacenarlos directamente en archivos de configuración o de
codificarlos de forma rígida.
Siga los pasos descritos en el tema Ubicación de almacenamiento segura de secretos de la aplicación en el
desarrollo de ASP.NET Core para poder almacenar los tokens asignados por cada uno de los siguientes
proveedores de inicio de sesión.

Configuración de los proveedores de inicio de sesión requeridos por


la aplicación
En los temas siguientes encontrará información para configurar la aplicación a fin de usar los proveedores
correspondientes:
Instrucciones para Facebook
Instrucciones para Twitter
Instrucciones para Google
Instrucciones para Microsoft
Instrucciones para otros proveedores
When the app requires multiple providers, chain the provider extension methods behind AddAuthentication:

services.AddAuthentication()
.AddMicrosoftAccount(microsoftOptions => { ... })
.AddGoogle(googleOptions => { ... })
.AddTwitter(twitterOptions => { ... })
.AddFacebook(facebookOptions => { ... });

Establecimiento opcional de contraseña


Si el registro se realiza mediante un proveedor de inicio de sesión externo, no se tiene ninguna contraseña en la
aplicación. De esta forma no hace falta crear y recordar una contraseña para el sitio, aunque le hace depender
del proveedor de inicio de sesión externo. Si el proveedor de inicio de sesión externo no está disponible, no
podrá iniciar sesión en el sitio web.
Para crear una contraseña e iniciar sesión con el correo electrónico establecido durante el proceso de inicio de
sesión con proveedores externos:
Pulse el vínculo Hola, <alias de correo electrónico> situado en la esquina superior derecha para ir a la
vista Administración.

Pulse Crear.

Establezca una contraseña válida. Podrá usarla para iniciar sesión con su correo electrónico.

Pasos siguientes
En este artículo se introdujo la autenticación externa y se explicaron los requisitos previos necesarios
para agregar inicios de sesión externos a la aplicación de ASP.NET Core.
Páginas de referencia específicas del proveedor para configurar los inicios de sesión para los
proveedores requeridos por la aplicación.
Programa de instalación de inicio de sesión externo
de Facebook en ASP.NET Core
22/06/2018 • 8 minutes to read • Edit Online

Por Valeriy Novytskyy y Rick Anderson


Este tutorial muestra cómo permitir a los usuarios iniciar sesión con su cuenta de Facebook mediante un proyecto
de ASP.NET Core 2.0 de ejemplo creado en el página anterior. Requiere la autenticación de Facebook el
Microsoft.AspNetCore.Authentication.Facebook paquete NuGet. Comenzamos creando un Facebook App ID
siguiendo el pasos oficiales.

Crear la aplicación de Facebook


Navegue hasta la aplicación de desarrolladores de Facebook página e inicie sesión. Si ya no tiene una
cuenta de Facebook, use la registrarse para Facebook vínculo en la página de inicio de sesión para crear
uno.
Pulse la agregar una nueva aplicación botón en la esquina superior derecha para crear un nuevo
identificador de aplicación.

Rellene el formulario y pulse el crear Id. de aplicación botón.

En el seleccionar un producto página, haga clic en Set Up en el inicio de sesión de Facebook tarjeta.
El inicio rápido asistente se iniciará con elegir una plataforma como la primera página. Omitir el
asistente por ahora, haga clic en el configuración vínculo en el menú de la izquierda:

Se le presentará la configuración de cliente OAuth página:


Escriba el URI de desarrollo con /signin-facebook anexan a la válido URI de redireccionamiento de OAuth
campo (por ejemplo: https://localhost:44320/signin-facebook ). La autenticación de Facebook configurada más
adelante en este tutorial controlará automáticamente las solicitudes en /signin-facebook ruta para implementar
el flujo de OAuth.

NOTE
El URI /signin-facebook se establece como la devolución de llamada predeterminada del proveedor de autenticación de
Facebook. Puede cambiar el URI de devolución de forma predeterminada al configurar el middleware de autenticación de
Facebook a través de los heredados RemoteAuthenticationOptions.CallbackPath propiedad de la FacebookOptions clase.

Haga clic en guardar cambios.


Haga clic en el panel vínculo en el panel de navegación izquierdo.
En esta página, tome nota de su App ID y su App Secret . Tanto en la aplicación de ASP.NET Core va a
agregar en la sección siguiente:
Al implementar el sitio debe volver a visitar el inicio de sesión de Facebook página de la instalación y
registrar un nuevo URI público.

Almacenar identificador de la aplicación de Facebook y secreto de la


aplicación
Vincular valores confidenciales como Facebook App ID y App Secret a su configuración de aplicación con el
secreto Manager. Para los fines de este tutorial, nombre de los tokens Authentication:Facebook:AppId y
Authentication:Facebook:AppSecret .

Ejecute los comandos siguientes para almacenar de forma segura App ID y App Secret con el Administrador de
secreto:

dotnet user-secrets set Authentication:Facebook:AppId <app-id>


dotnet user-secrets set Authentication:Facebook:AppSecret <app-secret>

Configurar la autenticación de Facebook


ASP.NET Core 2.x
ASP.NET Core 1.x
Agregue el servicio de Facebook en la ConfigureServices método en el Startup.cs archivo:

services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();

services.AddAuthentication().AddFacebook(facebookOptions =>
{
facebookOptions.AppId = Configuration["Authentication:Facebook:AppId"];
facebookOptions.AppSecret = Configuration["Authentication:Facebook:AppSecret"];
});

La llamada a AddIdentity configura las opciones de esquema predeterminadas. El AddAuthentication(String)


conjuntos de sobrecarga el DefaultScheme propiedad. El AddAuthentication (acción<AuthenticationOptions>)
sobrecarga permite configurar las opciones de autenticación, que se pueden usar para configurar los esquemas de
autenticación predeterminado para propósitos diferentes. Las llamadas subsiguientes a AddAuthentication
invalidación configurado previamente AuthenticationOptions propiedades.
AuthenticationBuilder métodos de extensión que registra un controlador de autenticación sólo se llama una vez
por cada esquema de autenticación. Las sobrecargas que permiten configurar las propiedades del esquema, el
nombre de esquema y nombre para mostrar existen.
When the app requires multiple providers, chain the provider extension methods behind AddAuthentication:
services.AddAuthentication()
.AddMicrosoftAccount(microsoftOptions => { ... })
.AddGoogle(googleOptions => { ... })
.AddTwitter(twitterOptions => { ... })
.AddFacebook(facebookOptions => { ... });

Consulte la FacebookOptions referencia de API para obtener más información sobre las opciones de
configuración compatible con la autenticación de Facebook. Opciones de configuración pueden utilizarse para:
Solicitar información diferente sobre el usuario.
Agregue los argumentos de cadena de consulta para personalizar la experiencia de inicio de sesión.

Inicie sesión con Facebook


Ejecute la aplicación y haga clic en sesión. Verá una opción para iniciar sesión con Facebook.

Al hacer clic en Facebook, se le redirigirá a Facebook para la autenticación:


Las solicitudes de autenticación de Facebook dirección pública de perfil y el correo electrónico de forma
predeterminada:
Una vez que escriba sus credenciales de Facebook que se le redirigirá al sitio donde puede establecer el correo
electrónico.
Ahora que haya iniciado sesión con sus credenciales de Facebook:

Solución de problemas
ASP.NET Core solo 2.x: identidad si no está configurado mediante una llamada a services.AddIdentity en
ConfigureServices , intenta autenticar se producirá en ArgumentException: se debe proporcionar la opción
'SignInScheme'. La plantilla de proyecto que se usan en este tutorial se asegura de que esto se realiza.
Si la base de datos de sitio no se ha creado mediante la aplicación de la migración inicial, obtendrá error en
una operación de base de datos al procesar la solicitud error. Pulse migraciones aplicar para crear la base de
datos y actualizar para continuar después del error.

Pasos siguientes
En este artículo se ha explicado cómo puede autenticar con Facebook. Puede seguir un enfoque similar
para autenticar con otros proveedores que se enumeran en la página anterior.
Una vez que se publica un sitio web a la aplicación web de Azure, debe restablecer la AppSecret en el
portal para desarrolladores de Facebook.
Establecer el Authentication:Facebook:AppId y Authentication:Facebook:AppSecret como configuración de
la aplicación en el portal de Azure. El sistema de configuración está configurado para leer las claves de las
variables de entorno.
Programa de instalación de inicio de sesión externo
de Twitter con ASP.NET Core
22/06/2018 • 7 minutes to read • Edit Online

Por Valeriy Novytskyy y Rick Anderson


Este tutorial muestra cómo permitir a los usuarios a iniciar sesión con su cuenta de Twitter usando un proyecto de
ASP.NET Core 2.0 de ejemplo creado en el página anterior.

Crear la aplicación en Twitter


Vaya a https://apps.twitter.com/ e inicie sesión. Si ya no tiene una cuenta de Twitter, use la Regístrese ahora
vínculo para crear uno. Después de iniciar sesión, el Application Management se muestra la página:

Pulse crear una aplicación nueva y rellene la aplicación nombre, descripción públicas y sitio Web (Esto
puede ser temporal hasta que el URI Registre el nombre de dominio):
Escriba el URI de desarrollo con /signin-twitter anexan a la válido URI de redireccionamiento de OAuth
campo (por ejemplo: https://localhost:44320/signin-twitter ). El esquema de autenticación de Twitter
configurado más adelante en este tutorial controlará automáticamente las solicitudes en /signin-twitter ruta
para implementar el flujo de OAuth.

NOTE
El segmento URI /signin-twitter se establece como la devolución de llamada predeterminada del proveedor de
autenticación de Twitter. Puede cambiar el URI de devolución de forma predeterminada al configurar el middleware de
autenticación de Twitter a través de los heredados RemoteAuthenticationOptions.CallbackPath propiedad de la
TwitterOptions clase.

Rellene el resto del formulario y pulse crear su aplicación de Twitter. Se muestran los detalles de la nueva
aplicación:
Al implementar el sitio que necesite volver a visitar el Application Management página y registrar un nuevo
URI público.

Almacenar Twitter ConsumerKey y ConsumerSecret


Vincular valores confidenciales como Twitter Consumer Key y Consumer Secret a su configuración de aplicación
con el secreto Manager. Para los fines de este tutorial, nombre de los tokens Authentication:Twitter:ConsumerKey
y Authentication:Twitter:ConsumerSecret .
Estos tokens se pueden encontrar en el claves y Tokens de acceso ficha después de crear la nueva aplicación de
Twitter:
Configurar la autenticación de Twitter
La plantilla de proyecto que se usan en este tutorial asegura de que Microsoft.AspNetCore.Authentication.Twitter
paquete ya está instalado.
Para instalar este paquete con 2017 de Visual Studio, haga doble clic en el proyecto y seleccione
administrar paquetes de NuGet.
Para instalar con CLI de .NET Core, ejecute lo siguiente en el directorio del proyecto:
dotnet add package Microsoft.AspNetCore.Authentication.Twitter

ASP.NET Core 2.x


ASP.NET Core 1.x
Agregue el servicio de Twitter en el ConfigureServices método Startup.cs archivo:

services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();

services.AddAuthentication().AddTwitter(twitterOptions =>
{
twitterOptions.ConsumerKey = Configuration["Authentication:Twitter:ConsumerKey"];
twitterOptions.ConsumerSecret = Configuration["Authentication:Twitter:ConsumerSecret"];
});

La llamada a AddIdentity configura las opciones de esquema predeterminadas. El AddAuthentication(String)


conjuntos de sobrecarga el DefaultScheme propiedad. El AddAuthentication (acción<AuthenticationOptions>)
sobrecarga permite configurar las opciones de autenticación, que se pueden usar para configurar los esquemas de
autenticación predeterminado para propósitos diferentes. Las llamadas subsiguientes a AddAuthentication
invalidación configurado previamente AuthenticationOptions propiedades.
AuthenticationBuilder métodos de extensión que registra un controlador de autenticación sólo se llama una vez
por cada esquema de autenticación. Las sobrecargas que permiten configurar las propiedades del esquema, el
nombre de esquema y nombre para mostrar existen.
When the app requires multiple providers, chain the provider extension methods behind AddAuthentication:

services.AddAuthentication()
.AddMicrosoftAccount(microsoftOptions => { ... })
.AddGoogle(googleOptions => { ... })
.AddTwitter(twitterOptions => { ... })
.AddFacebook(facebookOptions => { ... });

Consulte la TwitterOptions referencia de API para obtener más información sobre las opciones de configuración
compatible con autenticación de Twitter. Esto se puede usar para solicitar información diferente sobre el usuario.

Inicie sesión con Twitter


Ejecute la aplicación y haga clic en sesión. Aparece una opción para iniciar sesión con Twitter:

Al hacer clic en Twitter redirige a Twitter para la autenticación:


Después de escribir sus credenciales de Twitter, se le redirigirá al sitio web donde puede establecer el correo
electrónico.
Ahora que haya iniciado sesión con sus credenciales de Twitter:

Solución de problemas
ASP.NET Core solo 2.x: identidad si no está configurado mediante una llamada a services.AddIdentity en
ConfigureServices , intenta autenticar se producirá en ArgumentException: se debe proporcionar la opción
'SignInScheme'. La plantilla de proyecto que se usan en este tutorial se asegura de que esto se realiza.
Si la base de datos de sitio no se ha creado mediante la aplicación de la migración inicial, obtendrá error en
una operación de base de datos al procesar la solicitud error. Pulse migraciones aplicar para crear la base de
datos y actualizar para continuar después del error.
Pasos siguientes
En este artículo se ha explicado cómo puede autenticar con Twitter. Puede seguir un enfoque similar para
autenticar con otros proveedores que se enumeran en la página anterior.
Una vez que se publica un sitio web a la aplicación web de Azure, debe restablecer la ConsumerSecret en el
portal para desarrolladores de Twitter.
Establecer el Authentication:Twitter:ConsumerKey y Authentication:Twitter:ConsumerSecret como
configuración de la aplicación en el portal de Azure. El sistema de configuración está configurado para leer
las claves de las variables de entorno.
Programa de instalación de inicio de sesión externo
de Google en ASP.NET Core
22/06/2018 • 10 minutes to read • Edit Online

Por Valeriy Novytskyy y Rick Anderson


Este tutorial muestra cómo permitir a los usuarios iniciar sesión con su cuenta de Google + usando un proyecto
de ASP.NET Core 2.0 de ejemplo creado en el página anterior. Comenzamos siguiendo el pasos oficiales para
crear una nueva aplicación de consola de API de Google.

Crear la aplicación de consola de API de Google


Vaya a https://console.developers.google.com/projectselector/apis/library e inicie sesión. Si ya no tiene una
cuenta de Google, use más opciones > crear cuenta vínculo para crear una:

Se le redirigirá a biblioteca API Manager página:


Pulse crear y escriba su nombre del proyecto:

Después de aceptar el cuadro de diálogo, se le redirigirá a la página de la biblioteca que le permite elegir las
características de la aplicación nuevo. Buscar API de Google + en la lista y haga clic en el vínculo para agregar
la característica de API:
Se muestra la página de la API recién agregada. Pulse habilitar para agregar inicio de sesión de Google + en
la característica a la aplicación:

Después de habilitar la API, pulse crear credenciales para configurar los secretos:

Elija:
API de Google +
Servidor Web (por ejemplo, node.js, Tomcat), y
Datos de usuario:
Pulse las credenciales que es necesario? lo que irá al segundo paso de configuración de la aplicación, crear
un identificador de cliente de OAuth 2.0:
Dado que vamos a crear un proyecto de Google + con una sola característica (inicio de sesión), podemos
escribir el mismo nombre para el identificador de cliente de OAuth 2.0 que la que se utilizará para el
proyecto.
Escriba el URI de desarrollo con /signin-google anexan a la URI de redireccionamiento autorizados
campo (por ejemplo: https://localhost:44320/signin-google ). La autenticación de Google configurada más
adelante en este tutorial controlará automáticamente las solicitudes en /signin-google ruta para
implementar el flujo de OAuth.

NOTE
El segmento URI /signin-google se establece como la devolución de llamada predeterminada del proveedor de
autenticación de Google. Puede cambiar el URI de devolución de forma predeterminada al configurar el middleware de
autenticación de Google a través de los heredados RemoteAuthenticationOptions.CallbackPath propiedad de la
GoogleOptions clase.

Presione la tecla TAB para agregar el URI de redireccionamiento autorizados entrada.


Pulse crear ID de cliente, lo que irá con el tercer paso: configurar la pantalla de consentimiento de
OAuth 2.0:
Escriba el acceso público dirección de correo electrónico y nombre de producto se muestra para la
aplicación cuando Google + pide al usuario que inicie sesión en. Hay opciones adicionales disponibles en
más opciones de personalización.
Pulse continuar para continuar con el último paso, descargar credenciales:
Pulse descargar para guardar un archivo JSON con secretos de aplicación, y realiza para completar la
creación de la nueva aplicación.
Al implementar el sitio que necesite volver a visitar el consola de Google y registrar una nueva dirección
url pública.

Almacén Google ClientID y ClientSecret


Vincular valores confidenciales como Google Client ID y Client Secret a su configuración de aplicación con el
secreto Manager. Para los fines de este tutorial, nombre de los tokens Authentication:Google:ClientId y
Authentication:Google:ClientSecret .

Los valores para estos tokens se pueden encontrar en el archivo JSON que descargó en el paso anterior en
web.client_id y web.client_secret .

Configurar la autenticación de Google


ASP.NET Core 2.x
ASP.NET Core 1.x
Agregue el servicio de Google en el ConfigureServices método Startup.cs archivo:

services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();

services.AddAuthentication().AddGoogle(googleOptions =>
{
googleOptions.ClientId = Configuration["Authentication:Google:ClientId"];
googleOptions.ClientSecret = Configuration["Authentication:Google:ClientSecret"];
});

La llamada a AddIdentity configura las opciones de esquema predeterminadas. El AddAuthentication(String)


conjuntos de sobrecarga el DefaultScheme propiedad. El AddAuthentication (acción<AuthenticationOptions>)
sobrecarga permite configurar las opciones de autenticación, que se pueden usar para configurar los esquemas de
autenticación predeterminado para propósitos diferentes. Las llamadas subsiguientes a AddAuthentication
invalidación configurado previamente AuthenticationOptions propiedades.
AuthenticationBuilder métodos de extensión que registra un controlador de autenticación sólo se llama una vez
por cada esquema de autenticación. Las sobrecargas que permiten configurar las propiedades del esquema, el
nombre de esquema y nombre para mostrar existen.
When the app requires multiple providers, chain the provider extension methods behind AddAuthentication:

services.AddAuthentication()
.AddMicrosoftAccount(microsoftOptions => { ... })
.AddGoogle(googleOptions => { ... })
.AddTwitter(twitterOptions => { ... })
.AddFacebook(facebookOptions => { ... });

Consulte la GoogleOptions referencia de API para obtener más información sobre las opciones de configuración
compatible con autenticación de Google. Esto se puede usar para solicitar información diferente sobre el usuario.

Inicie sesión con Google


Ejecute la aplicación y haga clic en sesión. Aparece una opción para iniciar sesión con Google:

Al hacer clic en Google, se le redirigirá a Google para la autenticación:


Después de escribir sus credenciales de Google, a continuación, se le redirigirá al sitio web donde puede
establecer el correo electrónico.
Ahora que haya iniciado sesión con sus credenciales de Google:

Solución de problemas
Si recibe un 403 (Forbidden) página de error de la propia aplicación cuando se ejecuta en modo de desarrollo
(o interrupción en el depurador con el mismo error), asegúrese de que API de Google + se ha habilitado en el
biblioteca del Administrador de la API siguiendo los pasos enumerados anteriores en esta página. Si el
inicio de sesión no funciona y no reciben los errores, cambie al modo de desarrollo para que sea más fácil
depurar el problema.
ASP.NET Core solo 2.x: identidad si no está configurado mediante una llamada a services.AddIdentity en
ConfigureServices , intenta autenticar se producirá en ArgumentException: se debe proporcionar la opción
'SignInScheme'. La plantilla de proyecto que se usan en este tutorial se asegura de que esto se realiza.
Si la base de datos de sitio no se ha creado mediante la aplicación de la migración inicial, obtendrá error en
una operación de base de datos al procesar la solicitud error. Pulse migraciones aplicar para crear la base de
datos y actualizar para continuar después del error.
Pasos siguientes
En este artículo se ha explicado cómo puede autenticar con Google. Puede seguir un enfoque similar para
autenticar con otros proveedores que se enumeran en la página anterior.
Una vez que se publica un sitio web a la aplicación web de Azure, debe restablecer la ClientSecret en la
consola de API de Google.
Establecer el Authentication:Google:ClientId y Authentication:Google:ClientSecret como configuración de
la aplicación en el portal de Azure. El sistema de configuración está configurado para leer las claves de las
variables de entorno.
Programa de instalación de Microsoft Account inicio
de sesión externo con ASP.NET Core
22/06/2018 • 9 minutes to read • Edit Online

Por Valeriy Novytskyy y Rick Anderson


Este tutorial muestra cómo permitir a los usuarios iniciar sesión con su cuenta de Microsoft mediante un proyecto
de ASP.NET Core 2.0 de ejemplo creado en el página anterior.

Crear la aplicación en el Portal para desarrolladores de Microsoft


Vaya a https://apps.dev.microsoft.com y crear o iniciar sesión en una cuenta de Microsoft:

Si ya no tiene una cuenta de Microsoft, pulse crear uno. Después de iniciar sesión se le redirigirá a mis
aplicaciones página:

Pulse agregar una aplicación en la esquina superior derecha de las esquinas y escriba su nombre de la
aplicación y correo electrónico de contacto:

Para los fines de este tutorial, desactive el el programa de instalación interactiva casilla de verificación.
Pulse crear para continuar la registro página. Proporcionar un nombre y anote el valor de la Id. de
aplicación, que se utiliza como ClientId más adelante en el tutorial:
Pulse Agregar plataforma en el plataformas sección y seleccione el Web plataforma:
En el nuevo Web plataforma sección, escriba la dirección URL de desarrollo con /signin-microsoft anexan a
la redirigir direcciones URL campo (por ejemplo: https://localhost:44320/signin-microsoft ). El esquema de
autenticación de Microsoft configurado más adelante en este tutorial controlará automáticamente las
solicitudes en /signin-microsoft ruta para implementar el flujo de OAuth:

NOTE
El segmento URI /signin-microsoft se establece como la devolución de llamada predeterminada del proveedor de
autenticación de Microsoft. Puede cambiar el URI de devolución de forma predeterminada al configurar el middleware de
autenticación de Microsoft a través de los heredados RemoteAuthenticationOptions.CallbackPath propiedad de la
MicrosoftAccountOptions clase.

Pulse agregar dirección URL para asegurarse de que se agregó la dirección URL.
Rellene cualquier configuración de la aplicación si es necesario y pulse guardar en la parte inferior de la
página para guardar los cambios en la configuración de la aplicación.
Al implementar el sitio que necesite volver a visitar el registro página y establezca una nueva dirección
URL pública.

Almacenar identificador de la aplicación de Microsoft y la contraseña


Tenga en cuenta el Application Id muestra en el registro página.
Pulse generar nueva contraseña en el aplicación secretos sección. Se muestra un cuadro en el que
puede copiar la contraseña de aplicación:
Vincular valores confidenciales como Microsoft Application ID y Password a su configuración de aplicación con
el secreto Manager. Para los fines de este tutorial, nombre de los tokens Authentication:Microsoft:ApplicationId y
Authentication:Microsoft:Password .

Configurar la autenticación de cuenta de Microsoft


La plantilla de proyecto que se usan en este tutorial asegura de que
Microsoft.AspNetCore.Authentication.MicrosoftAccount paquete ya está instalado.
Para instalar este paquete con 2017 de Visual Studio, haga doble clic en el proyecto y seleccione
administrar paquetes de NuGet.
Para instalar con CLI de .NET Core, ejecute lo siguiente en el directorio del proyecto:
dotnet add package Microsoft.AspNetCore.Authentication.MicrosoftAccount

ASP.NET Core 2.x


ASP.NET Core 1.x
Agregue el servicio de Microsoft Account en el ConfigureServices método Startup.cs archivo:

services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();

services.AddAuthentication().AddMicrosoftAccount(microsoftOptions =>
{
microsoftOptions.ClientId = Configuration["Authentication:Microsoft:ApplicationId"];
microsoftOptions.ClientSecret = Configuration["Authentication:Microsoft:Password"];
});

La llamada a AddIdentity configura las opciones de esquema predeterminadas. El AddAuthentication(String)


conjuntos de sobrecarga el DefaultScheme propiedad. El AddAuthentication (acción<AuthenticationOptions>)
sobrecarga permite configurar las opciones de autenticación, que se pueden usar para configurar los esquemas de
autenticación predeterminado para propósitos diferentes. Las llamadas subsiguientes a AddAuthentication
invalidación configurado previamente AuthenticationOptions propiedades.
AuthenticationBuilder métodos de extensión que registra un controlador de autenticación sólo se llama una vez
por cada esquema de autenticación. Las sobrecargas que permiten configurar las propiedades del esquema, el
nombre de esquema y nombre para mostrar existen.
When the app requires multiple providers, chain the provider extension methods behind AddAuthentication:

services.AddAuthentication()
.AddMicrosoftAccount(microsoftOptions => { ... })
.AddGoogle(googleOptions => { ... })
.AddTwitter(twitterOptions => { ... })
.AddFacebook(facebookOptions => { ... });

Aunque la terminología utilizada en el Portal para desarrolladores de Microsoft nombres estos tokens
ApplicationId y Password , se halle expuestos como ClientId y ClientSecret a la API de configuración.

Consulte la MicrosoftAccountOptions referencia de API para obtener más información sobre las opciones de
configuración compatible con autenticación de Microsoft Account. Esto se puede usar para solicitar información
diferente sobre el usuario.

Inicie sesión con la cuenta de Microsoft


Ejecute la aplicación y haga clic en sesión. Aparece una opción para iniciar sesión con Microsoft:

Al hacer clic en Microsoft, se le redirigirá a Microsoft para la autenticación. Después de iniciar sesión con su
Account de Microsoft (si no lo ha hecho) se le pedirá para permitir que la aplicación acceder a su información:
Pulse Sí y se le redirigirá al sitio web donde puede establecer el correo electrónico.
Ahora que haya iniciado sesión con sus credenciales de Microsoft:

Solución de problemas
Si el proveedor de Microsoft Account le redirige a una página de error de inicio de sesión, tenga en cuenta
el error título y descripción de la cadena parámetros de consulta justo después del # (hashtag) en el Uri.
Aunque parezca que el mensaje de error indica un problema con la autenticación de Microsoft, la causa
más común es la aplicación Uri no coincide con ninguno de los URI de redireccionamiento especificado
para la Web plataforma .
ASP.NET Core solo 2.x: identidad si no está configurado mediante una llamada a services.AddIdentity
en ConfigureServices , intenta autenticar se producirá en ArgumentException: se debe proporcionar la
opción 'SignInScheme'. La plantilla de proyecto que se usan en este tutorial se asegura de que esto se
realiza.
Si la base de datos de sitio no se ha creado mediante la aplicación de la migración inicial, obtendrá error en
una operación de base de datos al procesar la solicitud error. Pulse migraciones aplicar para crear la base
de datos y actualizar para continuar después del error.
Pasos siguientes
En este artículo se ha explicado cómo puede autenticar con Microsoft. Puede seguir un enfoque similar
para autenticar con otros proveedores que se enumeran en la página anterior.
Una vez que se publica un sitio web a la aplicación web de Azure, debe crear un nuevo Password en el
Portal para desarrolladores de Microsoft.
Establecer el Authentication:Microsoft:ApplicationId y Authentication:Microsoft:Password como
configuración de la aplicación en el portal de Azure. El sistema de configuración está configurado para leer
las claves de las variables de entorno.
Breve encuesta de otros proveedores de
autenticación
22/06/2018 • 2 minutes to read • Edit Online

Por Rick Anderson, Pranav Rastogi, y Valeriy Novytskyy


Aquí se configuran las instrucciones para algunos proveedores OAuth comunes. Paquetes de NuGet de terceros,
como los que se mantiene por aspnet hogar puede usarse para complementar los proveedores de autenticación
implementados por el equipo de ASP.NET Core.
Configurar LinkedIn iniciar sesión en: https://www.linkedin.com/developer/apps . Vea pasos oficiales.
Configurar Instagram iniciar sesión en: https://www.instagram.com/developer/register/ . Vea pasos
oficiales.
Configurar Reddit iniciar sesión en: https://www.reddit.com/login?
dest=https%3A%2F%2Fwww.reddit.com%2Fprefs%2Fapps . Vea pasos oficiales.
Configurar Github iniciar sesión en: https://github.com/login?
return_to=https%3A%2F%2Fgithub.com%2Fsettings%2Fapplications%2Fnew . Vea pasos oficiales.
Configurar Yahoo iniciar sesión en: https://login.yahoo.com/config/login?
src=devnet&.done=http%3A%2F%2Fdeveloper.yahoo.com%2Fapps%2Fcreate%2F . Vea pasos oficiales.
Configurar Tumblr iniciar sesión en: https://www.tumblr.com/oauth/apps . Vea pasos oficiales.
Configurar Pinterest iniciar sesión en: https://www.pinterest.com/login/?
next=http%3A%2F%2Fdevsite%2Fapps%2F . Vea pasos oficiales.
Configurar Pocket iniciar sesión en: https://getpocket.com/developer/apps/new . Vea pasos oficiales.
Configurar Flickr iniciar sesión en: https://www.flickr.com/services/apps/create . Vea pasos oficiales.
Configurar Dribble iniciar sesión en: https://dribbble.com/signup . Vea pasos oficiales.
Configurar Vimeo iniciar sesión en: https://vimeo.com/join . Vea pasos oficiales.
Configurar SoundCloud iniciar sesión en: https://soundcloud.com/you/apps/new . Vea pasos oficiales.
Configurar VK iniciar sesión en: https://vk.com/apps?act=manage . Vea pasos oficiales.

Varios proveedores de autenticación


When the app requires multiple providers, chain the provider extension methods behind AddAuthentication:

services.AddAuthentication()
.AddMicrosoftAccount(microsoftOptions => { ... })
.AddGoogle(googleOptions => { ... })
.AddTwitter(twitterOptions => { ... })
.AddFacebook(facebookOptions => { ... });
Autenticar a los usuarios con WS-Federation en
ASP.NET Core
22/06/2018 • 7 minutes to read • Edit Online

Este tutorial muestra cómo permitir a los usuarios iniciar sesión con un proveedor de autenticación de WS -
Federation como Active Directory Federation Services (ADFS ) o Azure Active Directory (AAD ). Usa la aplicación
de ejemplo básica de ASP.NET 2.0 se describe en Facebook, Google y la autenticación de proveedor externo.
Para las aplicaciones de ASP.NET Core 2.0, ofrece compatibilidad con WS -Federation
Microsoft.AspNetCore.Authentication.WsFederation. Este componente se procede de
Microsoft.Owin.Security.WsFederation y comparte muchos de los mecanismos de ese componente. Sin embargo,
los componentes se diferencian en un par de aspectos importantes.
De forma predeterminada, el middleware nueva:
No permite que los inicios de sesión no solicitados. Esta característica del protocolo WS -Federation es
vulnerable a ataques XSRF. Sin embargo, se puede habilitar con el AllowUnsolicitedLogins opción.
No se comprueba cada formulario post para los mensajes de inicio de sesión. Solo se solicita a la CallbackPath
se comprueba el inicio de sesión complementos. CallbackPath tiene como valor predeterminado
/signin-wsfed pero puede cambiarse a través de los heredados RemoteAuthenticationOptions.CallbackPath
propiedad de la WsFederationOptions clase. Esta ruta de acceso se puede compartir con otros proveedores de
autenticación habilitando la SkipUnrecognizedRequests opción.

Registrar la aplicación con Active Directory


Servicios de federación de Active Directory
Abra el servidor entidad confiar en Asistente para agregar desde la consola de administración de AD FS:
Para escribir manualmente los datos, elija:

Escriba un nombre para mostrar para el usuario autenticado. El nombre no es importante para la aplicación
de ASP.NET Core.
Microsoft.AspNetCore.Authentication.WsFederation no es compatible con el cifrado de tokens, por lo que
no configure un certificado de cifrado de tokens:
Habilitar la compatibilidad de protocolo WS -Federation Passive, utilizando la dirección URL de la aplicación.
Compruebe que el puerto sea correcto para la aplicación:
NOTE
Debe ser una dirección URL HTTPS. IIS Express puede proporcionar un certificado autofirmado al hospedar la aplicación
durante el desarrollo. Kestrel requiere configuración manual de certificados. Consulte la documentación Kestrel para obtener
más detalles.

Haga clic en siguiente a través del resto del asistente y cerrar al final.
Identidad de núcleo ASP.NET requiere un Id. de nombre de notificación. Agregue uno de los editar reglas
de notificación cuadro de diálogo:
En el transformar notificaciones Asistente para agregar reglas, deje el valor predeterminado enviar
atributos LDAP como notificaciones plantilla seleccionada y haga clic en siguiente. Agregar una asignación
de regla el nombre de cuenta SAM atributo LDAP para la Id. de nombre notificación saliente:
Haga clic en finalizar > Aceptar en el editar reglas de notificación ventana.
Azure Active Directory
Vaya a la hoja de los registros de aplicación del inquilino AAD. Haga clic en nuevo registro de aplicación:

Escriba un nombre para el registro de aplicación. Esto no es importante para la aplicación de ASP.NET Core.
Escriba la dirección URL de la aplicación de escucha en que la dirección URL de inicio de sesión:
Haga clic en extremos y tenga en cuenta el documento de metadatos de federación dirección URL. Se
trata el middleware de WS -Federation MetadataAddress :

Navegue hasta el nuevo registro de aplicación. Haga clic en configuración > propiedades y tome nota de la
App ID URI. Se trata el middleware de WS -Federation Wtrealm :
Agregar WS-Federation como proveedor de inicio de sesión externo
para ASP.NET Core Identity
Agregue una dependencia en Microsoft.AspNetCore.Authentication.WsFederation al proyecto.
Agregar WS -Federation para la Configure método Startup.cs:

services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();

services.AddAuthentication()
.AddWsFederation(options =>
{
// MetadataAddress represents the Active Directory instance used to authenticate users.
options.MetadataAddress = "https://<ADFS FQDN or AAD tenant>/FederationMetadata/2007-
06/FederationMetadata.xml";

// Wtrealm is the app's identifier in the Active Directory instance.


// For ADFS, use the relying party's identifier, its WS-Federation Passive protocol URL:
options.Wtrealm = "https://localhost:44307/";

// For AAD, use the App ID URI from the app registration's Properties blade:
options.Wtrealm = "https://wsfedsample.onmicrosoft.com/bf0e7e6d-056e-4e37-b9a6-2c36797b9f01";
});

services.AddMvc()
// ...

La llamada a AddIdentity configura las opciones de esquema predeterminadas. El AddAuthentication(String)


conjuntos de sobrecarga el DefaultScheme propiedad. El AddAuthentication (acción<AuthenticationOptions>)
sobrecarga permite configurar las opciones de autenticación, que se pueden usar para configurar los esquemas de
autenticación predeterminado para propósitos diferentes. Las llamadas subsiguientes a AddAuthentication
invalidación configurado previamente AuthenticationOptions propiedades.
AuthenticationBuilder métodos de extensión que registra un controlador de autenticación sólo se llama una vez
por cada esquema de autenticación. Las sobrecargas que permiten configurar las propiedades del esquema, el
nombre de esquema y nombre para mostrar existen.
Inicie sesión con WS -Federation
Vaya a la aplicación y haga clic en el sesión vínculo en el encabezado de navegación. Hay una opción para iniciar
sesión con WsFederation:

Con AD FS como el proveedor, el botón se redirige a una página de inicio de sesión de AD FS:

Con Azure Active Directory como el proveedor, el botón se redirige a una página de inicio de sesión AAD:
Un inicio de sesión correcto en un nuevo usuario redirige a la página de registro de usuario de la aplicación:

Use WS-Federation sin identidad principal de ASP.NET


El middleware de WS -Federation se puede utilizar sin identidad. Por ejemplo:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultChallengeScheme = WsFederationDefaults.AuthenticationScheme;
})
.AddWsFederation(options =>
{
options.Wtrealm = Configuration["wsfed:realm"];
options.MetadataAddress = Configuration["wsfed:metadata"];
})
.AddCookie();
}

public void Configure(IApplicationBuilder app)


{
app.UseAuthentication();
// …
}
Confirmación de la cuenta y la recuperación de
contraseña en ASP.NET Core
22/06/2018 • 19 minutes to read • Edit Online

Por Rick Anderson y Joe Audette


Este tutorial muestra cómo compilar una aplicación de ASP.NET Core con el restablecimiento de confirmación y
la contraseña de correo electrónico. Este tutorial es no un tema de principio. Debe estar familiarizado con:
ASP.NET Core
Autenticación
Confirmación de cuentas y recuperación de contraseñas
Entity Framework Core
Vea este archivo PDF para las versiones 1.1 de MVC de ASP.NET Core y 2.x.

Requisitos previos
Install one of the following:
CLI tooling: Windows, Linux, or macOS: .NET Core SDK 2.0 or later
IDE/editor tooling
Windows: Visual Studio for Windows
ASP.NET and web development workload
.NET Core cross-platform development workload
Linux: Visual Studio Code
macOS: Visual Studio for Mac

Cree un nuevo proyecto de ASP.NET Core con la CLI de núcleo de


.NET
ASP.NET Core 2.x
ASP.NET Core 1.x

dotnet new webapp --auth Individual -o WebPWrecover


cd WebPWrecover

NOTE
In ASP.NET Core 2.1 or later, webapp is an alias of the razor argument. If the dotnet new webapp <OPTIONS> command
loads the dotnet new command help instead of creating a new Razor Pages app, install the .NET Core 2.1 SDK.

dotnet new razor --auth Individual -o WebPWrecover


cd WebPWrecover

--auth Individual Especifica la plantilla de proyecto de cuentas de usuario individuales.


En Windows, agregue el -uld opción. Especifica que LocalDB debe usarse en lugar de SQLite.
Ejecute new mvc --help para obtener ayuda sobre este comando.
Como alternativa, puede crear un nuevo proyecto de ASP.NET Core con Visual Studio:
En Visual Studio, cree un nuevo aplicación Web proyecto.
Seleccione principales de ASP.NET 2.0. .NET core está seleccionado en la siguiente imagen, pero puede
seleccionar .NET Framework.
Seleccione Cambiar autenticación y establezca en cuentas de usuario individuales.
Mantenga el valor predeterminado en la aplicación de cuentas de usuario de almacén.

Probar el nuevo registro de usuario


Ejecutar la aplicación, seleccione la registrar vincular y registrar un usuario. Siga las instrucciones para ejecutar
migraciones de Entity Framework Core. En este punto, es la única validación en el correo electrónico con el
[EmailAddress] atributo. Después de enviar el registro, se registran en la aplicación. Más adelante en el tutorial, el
código se actualiza para que los usuarios nuevos no se pueden iniciar sesión hasta que se ha validado el correo
electrónico.
Vista de la base de datos de identidad
Vea trabajar con código en un proyecto de MVC de ASP.NET Core para obtener instrucciones sobre cómo ver la
base de datos de SQLite.
Para Visual Studio:
Desde el vista menú, seleccione Explorador de objetos de SQL Server (SSOX).
Vaya a (localdb) MSSQLLocalDB (SQL Server 13). Haga doble clic en dbo. AspNetUsers > ver datos:

Tenga en cuenta la tabla EmailConfirmed campo es False .


Puede volver a usar este correo electrónico en el paso siguiente cuando la aplicación envía un correo electrónico
de confirmación. Haga doble clic en la fila y seleccione eliminar. Eliminar el alias de correo electrónico facilita en
los pasos siguientes.

Requerir HTTPS
Vea requieren HTTPS.

Requerir confirmación por correo electrónico


Es una práctica recomendada para confirmar el correo electrónico de un nuevo registro de usuario. Enviar por
correo electrónico de confirmación le ayuda a comprobar que no están suplantando otra persona (es decir, no ha
registrado con el correo electrónico de otra persona). Imagine que tuviera un foro de discusión, y desea evitar
"yli@example.com"al registrar como"nolivetto@contoso.com". Sin confirmación por correo electrónico,
"nolivetto@contoso.com" podría recibir correos electrónicos no deseados de la aplicación. Suponga que el
usuario registrado accidentalmente como "ylo@example.com" y no se vio el error ortográfico de "yli". No podrían
usar recuperación de contraseña porque la aplicación no tiene su correo electrónico correcto. Confirmación por
correo electrónico proporciona sólo una protección limitada de robots. Confirmación por correo electrónico no
proporciona protección de los usuarios malintencionados con varias cuentas de correo electrónico.
En general conveniente evitar que los nuevos usuarios se registren todos los datos del sitio web antes de que
tengan un correo electrónico confirmado.
Actualización ConfigureServices para requerir un correo electrónico confirmadas:

public void ConfigureServices(IServiceCollection services)


{
// Requires using Microsoft.AspNetCore.Mvc;
services.Configure<MvcOptions>(options =>
{
options.Filters.Add(new RequireHttpsAttribute());
});

services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

services.AddIdentity<ApplicationUser, IdentityRole>(config =>


{
config.SignIn.RequireConfirmedEmail = true;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();

services.AddMvc()
.AddRazorPagesOptions(options =>
{
options.Conventions.AuthorizeFolder("/Account/Manage");
options.Conventions.AuthorizePage("/Account/Logout");
});

config.SignIn.RequireConfirmedEmail = true; impide que los usuarios registrados el registro hasta que se ha
confirmado el correo electrónico.
Configurar el proveedor de correo electrónico
En este tutorial, se usa SendGrid para enviar correo electrónico. Necesita una cuenta de SendGrid y la clave para
enviar correo electrónico. Puede usar otros proveedores de correo electrónico. ASP.NET Core 2.x incluye
System.Net.Mail , que le permite enviar correo electrónico desde la aplicación. Se recomienda que usar SendGrid
u otro servicio de correo electrónico para enviar correo electrónico. SMTP es difícil proteger y configurado
correctamente.
El patrón opciones se usa para acceder a la configuración de cuenta y clave de usuario. Para obtener más
información, consulte configuración.
Cree una clase para obtener la clave de proteger el correo electrónico. En este ejemplo, el
AuthMessageSenderOptions se crea una clase en el Services/AuthMessageSenderOptions.cs archivo:

public class AuthMessageSenderOptions


{
public string SendGridUser { get; set; }
public string SendGridKey { get; set; }
}

Establecer el SendGridUser y SendGridKey con el herramienta Administrador de secreto. Por ejemplo:

C:\WebAppl\src\WebApp1>dotnet user-secrets set SendGridUser RickAndMSFT


info: Successfully saved SendGridUser = RickAndMSFT to the secret store.
En Windows, el Administrador de secreto almacena pares de claves/valor en un secrets.json un archivo en el
%APPDATA%/Microsoft/UserSecrets/<WebAppName-userSecretsId> directory.

El contenido de la secrets.json archivos no están cifrados. El secrets.json archivo se muestra a continuación (el
SendGridKey se ha quitado el valor.)

{
"SendGridUser": "RickAndMSFT",
"SendGridKey": "<key removed>"
}

Configurar el inicio para usar AuthMessageSenderOptions


Agregar AuthMessageSenderOptions al contenedor de servicios al final de la ConfigureServices método en el
Startup.cs archivo:
ASP.NET Core 2.x
ASP.NET Core 1.x

public void ConfigureServices(IServiceCollection services)


{
// Requires using Microsoft.AspNetCore.Mvc;
services.Configure<MvcOptions>(options =>
{
options.Filters.Add(new RequireHttpsAttribute());
});

services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

services.AddIdentity<ApplicationUser, IdentityRole>(config =>


{
config.SignIn.RequireConfirmedEmail = true;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();

services.AddMvc()
.AddRazorPagesOptions(options =>
{
options.Conventions.AuthorizeFolder("/Account/Manage");
options.Conventions.AuthorizePage("/Account/Logout");
});

services.AddSingleton<IEmailSender, EmailSender>();

services.Configure<AuthMessageSenderOptions>(Configuration);
}

Configurar la clase de AuthMessageSender


Este tutorial muestra cómo agregar notificaciones de correo electrónico a través de SendGrid, pero puede enviar
correo electrónico mediante SMTP y otros mecanismos.
Instalar el SendGrid paquete NuGet:
Desde la línea de comandos:
dotnet add package SendGrid

Desde la consola de administrador de paquetes, escriba el siguiente comando:


Install-Package SendGrid
Vea empiece de forma gratuita con SendGrid para registrar una cuenta gratuita de SendGrid.
Configurar SendGrid
ASP.NET Core 2.x
ASP.NET Core 1.x
Para configurar SendGrid, agregue código similar al siguiente en Services/EmailSender.cs:

using Microsoft.Extensions.Options;
using SendGrid;
using SendGrid.Helpers.Mail;
using System.Threading.Tasks;

namespace WebPWrecover.Services
{
public class EmailSender : IEmailSender
{
public EmailSender(IOptions<AuthMessageSenderOptions> optionsAccessor)
{
Options = optionsAccessor.Value;
}

public AuthMessageSenderOptions Options { get; } //set only via Secret Manager

public Task SendEmailAsync(string email, string subject, string message)


{
return Execute(Options.SendGridKey, subject, message, email);
}

public Task Execute(string apiKey, string subject, string message, string email)
{
var client = new SendGridClient(apiKey);
var msg = new SendGridMessage()
{
From = new EmailAddress("Joe@contoso.com", "Joe Smith"),
Subject = subject,
PlainTextContent = message,
HtmlContent = message
};
msg.AddTo(new EmailAddress(email));
return client.SendEmailAsync(msg);
}
}
}

Habilitar la recuperación de confirmación y la contraseña de cuenta


La plantilla tiene el código para la recuperación de confirmación y la contraseña de cuenta. Buscar el OnPostAsync
método Pages/Account/Register.cshtml.cs.
ASP.NET Core 2.x
ASP.NET Core 1.x
Impedir que los usuarios recién registrados que se iniciará automáticamente la sesión como comentario la línea
siguiente:

await _signInManager.SignInAsync(user, isPersistent: false);

El método completo se muestra con la línea cambiada resaltada:


public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
ReturnUrl = returnUrl;
if (ModelState.IsValid)
{
var user = new ApplicationUser { UserName = Input.Email, Email = Input.Email };
var result = await _userManager.CreateAsync(user, Input.Password);
if (result.Succeeded)
{
_logger.LogInformation("User created a new account with password.");

var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);


var callbackUrl = Url.EmailConfirmationLink(user.Id, code, Request.Scheme);
await _emailSender.SendEmailConfirmationAsync(Input.Email, callbackUrl);

// await _signInManager.SignInAsync(user, isPersistent: false);


return LocalRedirect(Url.GetLocalUrl(returnUrl));
}
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
}

// If we got this far, something failed, redisplay form


return Page();
}

Registrar, confirma el correo electrónico y restablecer contraseña


Ejecutar la aplicación web y probar la confirmación de la cuenta y el flujo de recuperación de contraseña.
Ejecutar la aplicación y registrar un nuevo usuario
Compruebe su correo electrónico para el vínculo de confirmación de cuenta. Vea depurar correo
electrónico si no recibe el correo electrónico.
Haga clic en el vínculo para confirmar tu correo electrónico.
Inicie sesión con su correo electrónico y contraseña.
Cierre la sesión.
Ver la página de administración
Seleccione el nombre de usuario en el explorador:

Debe expandir la barra de navegación para ver el nombre de usuario.

ASP.NET Core 2.x


ASP.NET Core 1.x
Se muestra la página de administración con el perfil pestaña seleccionada. El correo electrónico muestra una
casilla de verificación que indica el correo electrónico se ha confirmado.
Restablecimiento de contraseña de prueba
Si ha iniciado sesión, seleccione Logout.
Seleccione el sesión de vínculo y seleccione el ¿olvidó su contraseña? vínculo.
Escriba el correo electrónico que usa para registrar la cuenta.
Se envía un correo electrónico con un vínculo para restablecer su contraseña. Compruebe su correo
electrónico y haga clic en el vínculo para restablecer la contraseña. Después de que la contraseña se
restableció correctamente, puede iniciar sesión con su correo electrónico y la contraseña nueva.
Depurar el correo electrónico
Si no se puede obtener el trabajo de correo electrónico:
Crear un aplicación de consola para enviar correo electrónico.
Revise el actividad de correo electrónico página.
Compruebe la carpeta de correo basura.
Pruebe otro alias de correo electrónico en un proveedor de correo electrónico diferente (Microsoft, Yahoo,
Gmail, etcetera.)
Vuelva a enviar a las cuentas de correo electrónico diferente.
Una práctica recomendada de seguridad es no utilice secretos de producción en pruebas y desarrollo. Si
publica la aplicación en Azure, puede establecer los secretos de SendGrid como configuración de la aplicación en
el portal de la aplicación Web de Azure. El sistema de configuración está configurado para leer las claves de las
variables de entorno.

Combinar las cuentas de inicio de sesión locales y redes sociales


Para completar esta sección, primero debe habilitar a un proveedor de autenticación externo. Vea Facebook,
Google y la autenticación de proveedor externo.
Puede combinar cuentas locales y redes sociales, haga clic en el vínculo de correo electrónico. En la siguiente
secuencia, "RickAndMSFT@gmail.com" en primer lugar se crea como un inicio de sesión local; sin embargo,
puede crear la cuenta como un inicio de sesión social primero y luego agregar un inicio de sesión local.

Haga clic en el administrar vínculo. Tenga en cuenta el externo 0 (inicios de sesión sociales) asociados con esta
cuenta.

Haga clic en el vínculo a otro servicio de inicio de sesión y Aceptar las solicitudes de aplicación. En la siguiente
imagen, Facebook es el proveedor de autenticación externos:
Se han combinado las dos cuentas. Es posible iniciar sesión con cualquiera de estas cuentas. Puede que los
usuarios agreguen cuentas locales en caso de que su servicio de autenticación de inicio de sesión social está
inactivo o, más probablemente ha perdido acceso a su cuenta sociales.

Habilitar la confirmación de la cuenta después de un sitio tiene


usuarios
Habilitar confirmación de la cuenta en un sitio con usuarios bloquea todos los usuarios existentes. No tienen
acceso a los usuarios existentes porque no se ha confirmado sus cuentas. Para evitar el bloqueo de usuario
existente, use uno de los métodos siguientes:
Actualizar la base de datos para marcar todos los usuarios existentes como que se va a confirmar.
Confirme que los usuarios existentes. Por ejemplo, lote enviar correos electrónicos con vínculos de
confirmación.
Habilitar la generación de código QR para las
aplicaciones de autenticador de ASP.NET Core
22/06/2018 • 4 minutes to read • Edit Online

Nota: En este tema se aplica a ASP.NET Core 2.x


ASP.NET Core se suministra con compatibilidad para las aplicaciones de autenticador para la autenticación
individual. Dos aplicaciones de autenticador de autenticación (2FA) de factor, con una duración única contraseña
algoritmo (TOTP ), son el enfoque para 2FA recomendado en el sector. 2FA uso TOTP es preferible a 2FA SMS.
Una aplicación de autenticador proporciona un código de 6 a 8 dígitos que los usuarios deben escribir después de
confirmar su nombre de usuario y contraseña. Normalmente, una aplicación autenticadora está instalada en un
Smartphone.
Las plantillas de aplicación web de ASP.NET Core admiten autenticadores, pero no proporcionan una
compatibilidad para la generación de CódigoQR. Generadores de CódigoQR facilitan la configuración de 2FA.
Este documento le ayudará a agregar código QR generación a la página de configuración de 2FA.

Agregar códigos QR a la página de configuración de 2FA


Utilizan estas instrucciones qrcode.js de la https://davidshimjs.github.io/qrcodejs/ repo.
Descargue el qrcode.js javascript biblioteca a la wwwroot\lib carpeta del proyecto.
En Pages\Account\Manage\EnableAuthenticator.cshtml (las páginas de Razor) o
Views\Manage\EnableAuthenticator.cshtml (MVC ), busque la Scripts sección al final del archivo:

@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
}

Actualización de la Scripts sección para agregar una referencia a la qrcodejs biblioteca que agregó y una
llamada a generar el código QR. Debería ser como sigue:

@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")

<script type="text/javascript" src="~/lib/qrcode.js"></script>


<script type="text/javascript">
new QRCode(document.getElementById("qrCode"),
{
text: "@Html.Raw(Model.AuthenticatorUri)",
width: 150,
height: 150
});
</script>
}

Eliminar el párrafo que proporciona vínculos a estas instrucciones.


Ejecutar la aplicación y asegúrese de que puede examinar el código QR y validar el código que demuestra el
autenticador.
Cambiar el nombre del sitio en el código QR
El nombre del sitio en el código QR se toma del nombre del proyecto que elegir al crear inicialmente el proyecto.
Puede cambiarla si se busca la GenerateQrCodeUri(string email, string unformattedKey) método en el
Pages\Account\Manage\EnableAuthenticator.cshtml.cs archivo (las páginas de Razor) o la
Controllers\ManageController.cs archivo (MVC ).
El código predeterminado de la plantilla tiene el siguiente aspecto:

private string GenerateQrCodeUri(string email, string unformattedKey)


{
return string.Format(
AuthenicatorUriFormat,
_urlEncoder.Encode("Razor Pages"),
_urlEncoder.Encode(email),
unformattedKey);
}

El segundo parámetro en la llamada a string.Format es el nombre de sitio, tomado de su nombre de la solución.


Se puede cambiar a cualquier valor, pero debe ser siempre dirección URL codificada.

Utilizar una biblioteca de código QR diferente


Puede reemplazar la biblioteca de código QR por la biblioteca preferida. El código HTML contiene un qrCode
proporciona la biblioteca de elemento que se puede colocar un código QR mediante cualquier mecanismo.
La dirección URL con el formato correcto para el código QR está disponible en el:
AuthenticatorUri propiedad del modelo.
data-url propiedad en el qrCodeData elemento.

TOTP cliente y servidor sesgo horario


Autenticación de TOTP (basado en tiempo la contraseña de un solo uso) depende de dispositivo con el servidor y
el autenticador tiene una hora precisa. Símbolos (tokens) solo duran durante 30 segundos. Si se producen errores
en los inicios de sesión TOTP 2FA, compruebe que la hora del servidor es precisa y preferiblemente sincronizada
para un servicio NTP preciso.
Autenticación en dos fases con SMS en ASP.NET
Core
22/06/2018 • 9 minutes to read • Edit Online

Por Rick Anderson y desarrolladores suizo


Vea generación habilitar código QR para las aplicaciones de autenticador de ASP.NET Core para ASP.NET Core
2.0 y versiones posteriores.
Este tutorial muestra cómo configurar la autenticación en dos fases (2FA) con SMS. Se proporcionan
instrucciones para twilio y ASPSMS, pero puede usar cualquier otro proveedor SMS. Se recomienda realizar
confirmación de cuenta y contraseña de recuperación antes de iniciar este tutorial.
Ver el ejemplo completo. Cómo descargar.

Cree un nuevo proyecto de ASP.NET Core


Crear una nueva aplicación web de ASP.NET Core denominada Web2FA con cuentas de usuario individuales. Siga
las instrucciones de exigir SSL en una aplicación de ASP.NET Core para configurar y requerir SSL.
Crear una cuenta SMS
Crear una cuenta SMS, por ejemplo, de twilio o ASPSMS. Registre las credenciales de autenticación (para twilio:
accountSid y authToken para ASPSMS: clave de usuario confidenciales y la contraseña).
Pensar en las credenciales del proveedor de SMS
Twilio:
En la ficha Panel de su cuenta de Twilio, copie la SID de cuenta y token de autenticación.
ASPSMS:
Desde la configuración de su cuenta, vaya a clave de usuario confidenciales y cópielo junto con su
contraseña.
Más adelante se almacenará estos valores con la herramienta Administrador de secreto en el conjunto de claves
SMSAccountIdentification y SMSAccountPassword .

Especifica el identificador del remitente / originador


Twilio:
En la ficha números, copie su Twilio número de teléfono.
ASPSMS:
En el menú de remitentes desbloquear, desbloquear uno o más remitentes o elija un originador alfanumérico (no
admitido todas las redes).
Más adelante se almacenará este valor con la herramienta Administrador de secreto en la clave SMSAccountFrom .
Proporcione las credenciales para el servicio SMS
Vamos a usar la patrón opciones para tener acceso a la configuración de cuenta y clave de usuario.
Cree una clase para capturar la clave SMS segura. En este ejemplo, el SMSoptions se crea una clase en el
Services/SMSoptions.cs archivo.
namespace Web2FA.Services
{
public class SMSoptions
{
public string SMSAccountIdentification { get; set; }
public string SMSAccountPassword { get; set; }
public string SMSAccountFrom { get; set; }
}
}

Establecer el SMSAccountIdentification , SMSAccountPassword y SMSAccountFrom con el herramienta Administrador


de secreto. Por ejemplo:

C:/Web2FA/src/WebApp1>dotnet user-secrets set SMSAccountIdentification 12345


info: Successfully saved SMSAccountIdentification = 12345 to the secret store.

Agregue el paquete de NuGet del proveedor de SMS. Desde el paquete de administrador de consola (PMC )
ejecutar:
Twilio:
Install-Package Twilio

ASPSMS:
Install-Package ASPSMS

Agregue código en el Services/MessageServices.cs archivo para habilitar SMS. Utilice la Twilio o la sección
ASPSMS:
Twilio:
using Microsoft.Extensions.Options;
using System.Threading.Tasks;
using Twilio;
using Twilio.Rest.Api.V2010.Account;
using Twilio.Types;

namespace Web2FA.Services
{
// This class is used by the application to send Email and SMS
// when you turn on two-factor authentication in ASP.NET Identity.
// For more details see this link https://go.microsoft.com/fwlink/?LinkID=532713
public class AuthMessageSender : IEmailSender, ISmsSender
{
public AuthMessageSender(IOptions<SMSoptions> optionsAccessor)
{
Options = optionsAccessor.Value;
}

public SMSoptions Options { get; } // set only via Secret Manager

public Task SendEmailAsync(string email, string subject, string message)


{
// Plug in your email service here to send an email.
return Task.FromResult(0);
}

public Task SendSmsAsync(string number, string message)


{
// Plug in your SMS service here to send a text message.
// Your Account SID from twilio.com/console
var accountSid = Options.SMSAccountIdentification;
// Your Auth Token from twilio.com/console
var authToken = Options.SMSAccountPassword;

TwilioClient.Init(accountSid, authToken);

return MessageResource.CreateAsync(
to: new PhoneNumber(number),
from: new PhoneNumber(Options.SMSAccountFrom),
body: message);
}
}
}

ASPSMS:
using Microsoft.Extensions.Options;
using System.Threading.Tasks;

namespace Web2FA.Services
{
// This class is used by the application to send Email and SMS
// when you turn on two-factor authentication in ASP.NET Identity.
// For more details see this link https://go.microsoft.com/fwlink/?LinkID=532713
public class AuthMessageSender : IEmailSender, ISmsSender
{
public AuthMessageSender(IOptions<SMSoptions> optionsAccessor)
{
Options = optionsAccessor.Value;
}

public SMSoptions Options { get; } // set only via Secret Manager

public Task SendEmailAsync(string email, string subject, string message)


{
// Plug in your email service here to send an email.
return Task.FromResult(0);
}

public Task SendSmsAsync(string number, string message)


{
ASPSMS.SMS SMSSender = new ASPSMS.SMS();

SMSSender.Userkey = Options.SMSAccountIdentification;
SMSSender.Password = Options.SMSAccountPassword;
SMSSender.Originator = Options.SMSAccountFrom;

SMSSender.AddRecipient(number);
SMSSender.MessageData = message;

SMSSender.SendTextSMS();

return Task.FromResult(0);
}
}
}

Configurar el inicio de usar SMSoptions

Agregar SMSoptions al contenedor de servicios en la ConfigureServices método en el Startup.cs:

// Add application services.


services.AddTransient<IEmailSender, AuthMessageSender>();
services.AddTransient<ISmsSender, AuthMessageSender>();
services.Configure<SMSoptions>(Configuration);
}

Habilitar la autenticación en dos fases


Abra la Views/Manage/Index.cshtml archivo de vista Razor y quite el comentario de caracteres (por lo que ningún
tipo de marcado es almohadilla).

Inicie sesión con la autenticación en dos fases


Ejecutar la aplicación y registrar un nuevo usuario
Puntee en el nombre de usuario, activa la Index métodos de acción de controlador de administrar. A
continuación, puntee en el número de teléfono agregar vínculo.

Agregar un número de teléfono que recibirá el código de comprobación y pulse enviar código de
comprobación.
Obtendrá un mensaje de texto con el código de comprobación. Escríbala y pulse enviar

Si no recibe un mensaje de texto, consulte la página de registro de twilio.


La vista gestionar muestra que el número de teléfono se agregó correctamente.
Pulse habilitar para habilitar la autenticación en dos fases.

Autenticación de dos factores de prueba


Cierre la sesión.
Inicia sesión.
La cuenta de usuario ha habilitado la autenticación en dos fases, por lo que tendrá que proporcionar el
segundo factor de autenticación. En este tutorial se ha habilitado la verificación por teléfono. Las plantillas
creadas en también le permiten configurar el correo electrónico como el segundo factor. Puede configurar
factores de segundo adicionales para la autenticación como códigos QR. Pulse enviar.

Escriba el código que se obtienen en el mensaje SMS.


Al hacer clic en el recordar este explorador casilla de verificación se excluya de la necesidad de usar 2FA
para iniciar sesión cuando se usa el mismo dispositivo y el explorador. Habilitar 2FA y haciendo clic en
recordar este explorador le proporcionará protección segura 2FA de usuarios malintencionados que
intenta acceder a su cuenta, siempre y cuando no tienen acceso al dispositivo. Puede hacerlo en cualquier
dispositivo privada que se usan con frecuencia. Estableciendo recordar este explorador, obtener la
seguridad adicional de 2FA desde dispositivos que no use con regularidad y obtener la comodidad de no
tener que pasar por 2FA en sus propios dispositivos.
Bloqueo de cuenta para protegerse contra los ataques por fuerza
bruta
Se recomienda el bloqueo de cuenta con 2FA. Una vez que un usuario inicia sesión a través de una cuenta local o
sociales, se almacena cada intento fallido en 2FA. Si se alcanza los intentos de acceso erróneos máximo, el usuario
está bloqueado (valor predeterminado: bloqueo de 5 minutos después de 5 intentos de acceso). Una autenticación
correcta restablece el número de intentos de acceso erróneos y restablece el reloj. El valor máximo intentos de
acceso y se puede establecer el tiempo de bloqueo con MaxFailedAccessAttempts y DefaultLockoutTimeSpan. El
siguiente ejemplo configura el bloqueo de cuenta de 10 minutos tras 10 intentos de acceso:

public void ConfigureServices(IServiceCollection services)


{
// Add framework services.
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();

services.AddMvc();

services.Configure<IdentityOptions>(options =>
{
options.Lockout.MaxFailedAccessAttempts = 10;
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(10);
});

// Add application services.


services.AddTransient<IEmailSender, AuthMessageSender>();
services.AddTransient<ISmsSender, AuthMessageSender>();
services.Configure<SMSoptions>(Configuration);
}

Confirme que PasswordSignInAsync establece lockoutOnFailure a true :


var result = await _signInManager.PasswordSignInAsync(
Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: true);
Usar autenticación con cookies sin ASP.NET Core
Identity
22/06/2018 • 30 minutes to read • Edit Online

Por Rick Anderson y Luke Latham


Como ha visto en los temas de autenticación anteriores, ASP.NET Core Identity es un proveedor de autenticación
completo y completa para crear y mantener los inicios de sesión. Sin embargo, puede que desee utilizar su propia
lógica de autenticación personalizada con la autenticación basada en cookies a veces. Puede usar la autenticación
basada en cookies como un proveedor de autenticación independiente sin ASP.NET Core Identity.
Vea o descargue el código de ejemplo (cómo descargarlo)
Para fines de demostración de la aplicación de ejemplo, la cuenta de usuario para el usuario hipotética, Maria
Rodríguez, está codificada en la aplicación. Use el nombre de usuario de correo electrónico
"maria.rodriguez@contoso.com" y una contraseña para iniciar sesión en el usuario. El usuario se autentica en el
AuthenticateUser método en el Pages/Account/Login.cshtml.cs archivo. En un ejemplo del mundo real, se debería
autenticar el usuario en una base de datos.
Para obtener información sobre migración autenticación basada en cookies de ASP.NET Core 1.x a 2.0, consulte
migrar autenticación e identidad al tema principal de ASP.NET 2.0 (autenticación basada en cookies).
Para usar la identidad de núcleo de ASP.NET, vea el Introducción a la identidad tema.

Configuración
ASP.NET Core 2.x
ASP.NET Core 1.x
Si la aplicación no usa el Microsoft.AspNetCore.App metapackage, cree una referencia de paquete en el archivo
de proyecto para la Microsoft.AspNetCore.Authentication.Cookies paquete (versión 2.1.0 o más adelante).
En el ConfigureServices (método), crear el servicio de Middleware de autenticación con el AddAuthentication y
AddCookie métodos:

services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie();

AuthenticationScheme pasa al establece el esquema de autenticación predeterminado para la


AddAuthentication
aplicación. AuthenticationScheme es útil cuando hay varias instancias de autenticación con cookies y desea
autorizarse con un esquema específico. Establecer el AuthenticationScheme a
CookieAuthenticationDefaults.AuthenticationScheme proporciona un valor de "Cookies" para el esquema. Puede
proporcionar cualquier valor de cadena que distingue el esquema.
En el Configure método, use la UseAuthentication método que se invoca el Middleware de autenticación que
establece el HttpContext.User propiedad. Llame a la UseAuthentication método antes de llamar a
UseMvcWithDefaultRoute o UseMvc :

app.UseAuthentication();
Opciones de AddCookie
El CookieAuthenticationOptions clase se utiliza para configurar las opciones de proveedor de autenticación.

OPCIÓN DESCRIPCIÓN

AccessDeniedPath Proporciona la ruta de acceso para suministrar con un 302


encontrado (redireccionamiento de la dirección URL) cuando
se desencadena por HttpContext.ForbidAsync . El valor
predeterminado es /Account/AccessDenied .

ClaimsIssuer El emisor que se usará para la emisor propiedad en las


notificaciones creados por el servicio de autenticación de la
cookie.

Cookie.Domain El nombre de dominio donde se sirve la cookie. De forma


predeterminada, este es el nombre de host de la solicitud. El
explorador sólo envía la cookie en las solicitudes a un nombre
de host coincidente. Puede que desee ajustar esta opción para
que las cookies disponibles para todos los hosts en el
dominio. Por ejemplo, establecer el dominio de cookies
.contoso.com pone a disposición contoso.com ,
www.contoso.com , y staging.www.contoso.com .

Cookie.Expiration Obtiene o establece el tiempo de vida de una cookie.


Actualmente, esta opción no ops y quedarán obsoleta en
ASP.NET Core 2.1 +. Use la ExpireTimeSpan opción para
establecer la expiración de la cookie. Para obtener más
información, consulte aclarar el comportamiento de
CookieAuthenticationOptions.Cookie.Expiration.

Cookie.HttpOnly Una marca que indica si la cookie debe ser accesible sólo a los
servidores. Si cambia este valor a false permite que los
scripts del lado cliente para tener acceso a la cookie y se
puede abrir la aplicación al robo de cookies debe tener la
aplicación un scripting entre sitios (XSS) una vulnerabilidad. El
valor predeterminado es true .

Cookie.Name Establece el nombre de la cookie.

Cookie.Path Se utiliza para aislar las aplicaciones que se ejecutan en el


mismo nombre de host. Si tiene aplicaciones que se ejecutan
en /app1 y desea restringir las cookies a esa aplicación,
establezca el CookiePath propiedad /app1 . Al hacerlo, la
cookie solo está disponible en las solicitudes a /app1 y
cualquier aplicación aparecen debajo de él.

Cookie.SameSite Indica si el explorador debe permitir la cookie que se


adjuntará a sólo las solicitudes del mismo sitio (
SameSiteMode.Strict ) o solicitudes entre sitios mediante
métodos de prueba de errores HTTP y solicitudes del mismo
sitio ( SameSiteMode.Lax ). Cuando se establece en
SameSiteMode.None , no se establece el valor del encabezado
de cookie. Tenga en cuenta que Middleware de cookies
directiva podría sobrescribir el valor que se proporcione. Para
admitir la autenticación de OAuth, el valor predeterminado es
SameSiteMode.Lax . Para obtener más información, consulte
roto debido a la directiva de SameSite cookie de autenticación
de OAuth.
OPCIÓN DESCRIPCIÓN

Cookie.SecurePolicy Una marca que indica si la cookie creada debe limitarse a


HTTPS ( CookieSecurePolicy.Always ), HTTP o HTTPS (
CookieSecurePolicy.None ), o el mismo protocolo que la
solicitud ( CookieSecurePolicy.SameAsRequest ). El valor
predeterminado es CookieSecurePolicy.SameAsRequest .

DataProtectionProvider Establece el DataProtectionProvider que se utiliza para


crear el valor predeterminado TicketDataFormat . Si el
TicketDataFormat propiedad está establecida, el
DataProtectionProvider opción no se utiliza. Si no se
proporciona, se utiliza el proveedor de protección de datos de
la aplicación predeterminada.

Eventos El controlador llama a métodos en el proveedor que


proporcionan el control de la aplicación en determinados
puntos de procesamiento. Si Events no siempre, se
proporciona una instancia predeterminada que no hace nada
cuando se llaman a los métodos.

EventsType Utiliza como el tipo de servicio para obtener el Events


instancia en lugar de la propiedad.

ExpireTimeSpan El TimeSpan tras el cual expira el vale de autenticación que


se almacena dentro de la cookie. ExpireTimeSpan se agrega
a la hora actual para crear la fecha de expiración para el vale.
El ExpiredTimeSpan siempre que el valor sea en el cifrado
AuthTicket verificada por el servidor. También puede ir a la
Set-Cookie encabezado, pero solo si IsPersistent se
establece. Para establecer IsPersistent a true , configurar
la AuthenticationProperties pasado a SignInAsync . El valor
predeterminado de ExpireTimeSpan es 14 días.

LoginPath Proporciona la ruta de acceso para suministrar con un 302


encontrado (redireccionamiento de la dirección URL) cuando
se desencadena por HttpContext.ChallengeAsync . La
dirección URL actual que generó el 401 se agrega a la
LoginPath como un parámetro de cadena de consulta
denominado por la ReturnUrlParameter . Una vez una
solicitud para la LoginPath concede una nueva identidad de
inicio de sesión, la ReturnUrlParameter valor se utiliza para
redirigir el explorador a la dirección URL que produjo el código
de estado sin autorización original. El valor predeterminado es
/Account/Login .

LogoutPath Si el LogoutPath se proporciona al controlador, a


continuación, redirige una solicitud a dicha ruta de acceso en
función del valor de la ReturnUrlParameter . El valor
predeterminado es /Account/Logout .
OPCIÓN DESCRIPCIÓN

ReturnUrlParameter Determina el nombre del parámetro de cadena de consulta


que se anexa el controlador para una respuesta 302 de Found
(redireccionamiento de la dirección URL).
ReturnUrlParameter se utiliza cuando llega una solicitud en
el LoginPath o LogoutPath para devolver el explorador a
la dirección URL original después de realiza la acción de inicio
de sesión o cierre de sesión. El valor predeterminado es
ReturnUrl .

SessionStore Contenedor opcional que se utiliza para almacenar la


identidad todas las solicitudes. Cuando se utiliza, solo un
identificador de sesión se envía al cliente. SessionStore
puede utilizarse para mitigar los posibles problemas con
identidades grandes.

slidingExpiration Una marca que indica si se debe emitir una nueva cookie con
una fecha de caducidad actualizada dinámicamente. Esto
puede suceder en cualquier solicitud que el período de
expiración de cookie actual es más del 50% expira. La nueva
fecha de expiración se mueve hacia delante como la fecha
actual más el ExpireTimespan . Un tiempo de expiración de
cookie absoluta puede establecerse mediante el
AuthenticationProperties al llamar a la clase
SignInAsync . Una hora de expiración absoluta puede
mejorar la seguridad de la aplicación mediante la limitación de
la cantidad de tiempo que la cookie de autenticación es válida.
El valor predeterminado es true .

TicketDataFormat El TicketDataFormat se usa para proteger y desproteger la


identidad y otras propiedades que se almacenan en el valor
de cookie. Si no se proporciona un TicketDataFormat se
crea utilizando el DataProtectionProvider.

Validar Método que compruebe que las opciones son válidas.

Establecer CookieAuthenticationOptions en la configuración del servicio para la autenticación en el


ConfigureServices método:

services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
...
});

Middleware de cookies directiva.


Middleware de cookies directiva habilita las capacidades de directiva de cookie de una aplicación. Agregar el
middleware a la canalización de procesamiento de la aplicación es el orden de minúsculas; solo afecta a los
componentes registrados después de él en la canalización.

app.UseCookiePolicy(cookiePolicyOptions);

El CookiePolicyOptions proporcionado para el Middleware de directiva de Cookie le permiten controlar


características globales del procesamiento de la cookie y el enlace en los controladores de procesamiento de
cookie cuando se agrega o se eliminan las cookies.

PROPERTY DESCRIPCIÓN

HttpOnly Afecta a si las cookies deben estar HttpOnly, que es una


marca que indica si la cookie debe ser accesible sólo a los
servidores. El valor predeterminado es
HttpOnlyPolicy.None .

MinimumSameSitePolicy Afecta al atributo del mismo sitio de la cookie (ver abajo). El


valor predeterminado es SameSiteMode.Lax . Esta opción
está disponible para el núcleo de ASP.NET 2.0 +.

OnAppendCookie Se llama cuando se anexa una cookie.

OnDeleteCookie Se llama cuando se elimina una cookie.

Proteger Determina si las cookies deben estar seguro. El valor


predeterminado es CookieSecurePolicy.None .

MinimumSameSitePolicy (ASP.NET 2.0 + sólo principal)


El valor predeterminado MinimumSameSitePolicy valor es SameSiteMode.Lax para permitir la autenticación de
OAuth2. Estrictamente aplicar una directiva del mismo sitio de SameSiteMode.Strict , establezca el
MinimumSameSitePolicy . Aunque esta configuración interrumpe OAuth2 y otros esquemas de autenticación entre
orígenes, eleva el nivel de seguridad de la cookie para otros tipos de aplicaciones que no confían en el
procesamiento de solicitudes entre orígenes.

var cookiePolicyOptions = new CookiePolicyOptions


{
MinimumSameSitePolicy = SameSiteMode.Strict,
};

La configuración de directiva Middleware de cookies para MinimumSameSitePolicy pueden afectar a la


configuración de Cookie.SameSite en CookieAuthenticationOptions valores según la tabla siguiente.

CONFIGURACIÓN DE COOKIE.SAMESITE
MINIMUMSAMESITEPOLICY COOKIE.SAMESITE RESULTANTE

SameSiteMode.None SameSiteMode.None SameSiteMode.None


SameSiteMode.Lax SameSiteMode.Lax
SameSiteMode.Strict SameSiteMode.Strict

SameSiteMode.Lax SameSiteMode.None SameSiteMode.Lax


SameSiteMode.Lax SameSiteMode.Lax
SameSiteMode.Strict SameSiteMode.Strict

SameSiteMode.Strict SameSiteMode.None SameSiteMode.Strict


SameSiteMode.Lax SameSiteMode.Strict
SameSiteMode.Strict SameSiteMode.Strict

Crear una cookie de autenticación


Para crear una cookie que contiene información de usuario, debe construir un ClaimsPrincipal. La información de
usuario se serializa y se almacena en la cookie.
ASP.NET Core 2.x
ASP.NET Core 1.x
Crear un ClaimsIdentity con cualquier necesario notificacións y llame al método SignInAsync para iniciar sesión
en el usuario:

var claims = new List<Claim>


{
new Claim(ClaimTypes.Name, user.Email),
new Claim("FullName", user.FullName),
new Claim(ClaimTypes.Role, "Administrator"),
};

var claimsIdentity = new ClaimsIdentity(


claims, CookieAuthenticationDefaults.AuthenticationScheme);

var authProperties = new AuthenticationProperties


{
//AllowRefresh = <bool>,
// Refreshing the authentication session should be allowed.

//ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(10),
// The time at which the authentication ticket expires. A
// value set here overrides the ExpireTimeSpan option of
// CookieAuthenticationOptions set with AddCookie.

//IsPersistent = true,
// Whether the authentication session is persisted across
// multiple requests. Required when setting the
// ExpireTimeSpan option of CookieAuthenticationOptions
// set with AddCookie. Also required when setting
// ExpiresUtc.

//IssuedUtc = <DateTimeOffset>,
// The time at which the authentication ticket was issued.

//RedirectUri = <string>
// The full path or absolute URI to be used as an http
// redirect response value.
};

await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity),
authProperties);

SignInAsync crea una cookie cifrada y lo agrega a la respuesta actual. Si no se especifica un AuthenticationScheme
, se utiliza el esquema predeterminado.
Tras los bastidores, el cifrado usado es ASP.NET Core protección de datos sistema. Si aloja la aplicación en varios
equipos, equilibrio de carga entre las aplicaciones o usar una granja de servidores web, debe configurar la
protección de datos para usar el mismo anillo de clave y el identificador de la aplicación.

Cerrar sesión
ASP.NET Core 2.x
ASP.NET Core 1.x
Para cerrar la sesión del usuario actual y eliminar las cookies, llame a SignOutAsync:
await HttpContext.SignOutAsync(
CookieAuthenticationDefaults.AuthenticationScheme);

Si no está usando CookieAuthenticationDefaults.AuthenticationScheme (o "Cookies") como el esquema (por


ejemplo, "ContosoCookie"), proporcione el esquema que usó al configurar el proveedor de autenticación. En caso
contrario, se utiliza el esquema predeterminado.

Reaccione ante los cambios de back-end


Una vez que se crea una cookie, se convierte en el único origen de identidad. Incluso si se deshabilita a un usuario
en los sistemas back-end, el sistema de autenticación de cookie no tiene ningún conocimiento de este, y un
usuario permanece ha iniciado sesión como su cookie es válida.
El ValidatePrincipal eventos en ASP.NET Core 2.x o ValidateAsync método en ASP.NET Core 1.x puede usarse
para interceptar y reemplazar la validación de la identidad de la cookie. Este enfoque reduce el riesgo de
revocados a los usuarios obtener acceso a la aplicación.
Un enfoque de validación de la cookie se basa en realizar el seguimiento de cuándo se ha modificado la base de
datos de usuario. Si la base de datos no se ha modificado desde que se emitió la cookie del usuario, no hay
ninguna necesidad de volver a autenticar al usuario si su cookie sigue siendo válido. Para implementar este
escenario, la base de datos, que se implementa en IUserRepository en este ejemplo, almacena una LastChanged
valor. Cuando cualquier usuario se actualiza en la base de datos, la LastChanged valor se establece en la hora
actual.
Con el fin de invalidar una cookie cuando los cambios de la base de datos se basan en el LastChanged valor, cree
la cookie con un LastChanged notificación que contiene el actual LastChanged valor de la base de datos:

var claims = new List<Claim>


{
new Claim(ClaimTypes.Name, user.Email),
new Claim("LastChanged", {Database Value})
};

var claimsIdentity = new ClaimsIdentity(


claims,
CookieAuthenticationDefaults.AuthenticationScheme);

await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity));

ASP.NET Core 2.x


ASP.NET Core 1.x
Para implementar una invalidación para el ValidatePrincipal eventos, escribir un método con la siguiente firma
en una clase que derive de CookieAuthenticationEvents:

ValidatePrincipal(CookieValidatePrincipalContext)

Un ejemplo es similar a lo siguiente:


using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;

public class CustomCookieAuthenticationEvents : CookieAuthenticationEvents


{
private readonly IUserRepository _userRepository;

public CustomCookieAuthenticationEvents(IUserRepository userRepository)


{
// Get the database from registered DI services.
_userRepository = userRepository;
}

public override async Task ValidatePrincipal(CookieValidatePrincipalContext context)


{
var userPrincipal = context.Principal;

// Look for the LastChanged claim.


var lastChanged = (from c in userPrincipal.Claims
where c.Type == "LastChanged"
select c.Value).FirstOrDefault();

if (string.IsNullOrEmpty(lastChanged) ||
!_userRepository.ValidateLastChanged(lastChanged))
{
context.RejectPrincipal();

await context.HttpContext.SignOutAsync(
CookieAuthenticationDefaults.AuthenticationScheme);
}
}
}

Registrar la instancia de eventos durante el registro de servicio de cookie en el ConfigureServices método.


Proporcionar un registro de servicio con ámbito para su CustomCookieAuthenticationEvents clase:

services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.EventsType = typeof(CustomCookieAuthenticationEvents);
});

services.AddScoped<CustomCookieAuthenticationEvents>();

Considere la posibilidad de una situación en la que se actualiza el nombre del usuario — una decisión que no
afectan a la seguridad de ninguna manera. Si desea actualizar la entidad de seguridad de usuario de manera no
destructiva, llame a context.ReplacePrincipal y establezca el context.ShouldRenew propiedad true .

WARNING
El enfoque descrito aquí se desencadena en cada solicitud. Esto puede dar lugar a una reducción del rendimiento de gran
tamaño de la aplicación.

Cookies persistentes
Puede que desee la cookie que se conservan entre sesiones del explorador. Esta persistencia solo debería
habilitarse con el consentimiento del usuario explícita con una casilla "Recordar mi cuenta" en Inicio de sesión o
un mecanismo similar.
El fragmento de código siguiente crea una identidad y cookie correspondiente que sobrevive a través de los
cierres de explorador. Se respetan los parámetros de expiración deslizante configurados previamente. Si la cookie
expira mientras se cierra el explorador, el explorador borra la cookie de una vez que se reinicie.
ASP.NET Core 2.x
ASP.NET Core 1.x

await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity),
new AuthenticationProperties
{
IsPersistent = true
});

El AuthenticationProperties clase reside en el Microsoft.AspNetCore.Authentication espacio de nombres.

Expiración de cookie absoluta


Puede establecer un tiempo de expiración absoluta con ExpiresUtc . También debe establecer IsPersistent ; en
caso contrario, ExpiresUtc se omite y se crea una cookie de sesión único. Cuando ExpiresUtc está establecido en
SignInAsync , invalida el valor de la ExpireTimeSpan opción de CookieAuthenticationOptions , si se establece.

El fragmento de código siguiente crea una identidad y cookie correspondiente que tiene una validez de 20
minutos. Esto omite cualquier configuración de expiración deslizante configurado previamente.
ASP.NET Core 2.x
ASP.NET Core 1.x

await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity),
new AuthenticationProperties
{
IsPersistent = true,
ExpiresUtc = DateTime.UtcNow.AddMinutes(20)
});

Recursos adicionales
Cambios de autenticación 2.0 / anuncio de migración
Limitación de la identidad por esquema
Autorización basada en notificaciones
Comprobaciones de la función basada en directivas
Azure Active Directory con ASP.NET Core
21/06/2018 • 2 minutes to read • Edit Online

Ejemplos de Azure AD V1
En los ejemplos siguientes se muestra cómo integrar Azure AD V1, lo que permite a los usuarios iniciar sesión con
una cuenta profesional o educativa:
Integración de Azure AD en una aplicación web de ASP.NET Core
Llamada a una API Web de ASP.NET Core desde una aplicación de WPF con Azure AD
Llamada a una API Web en una aplicación web de ASP.NET Core con Azure AD

Ejemplos de Azure AD V2
En los ejemplos siguientes se muestra cómo integrar Azure AD V2, lo que permite a los usuarios iniciar sesión con
una cuenta profesional o educativa o con una cuenta de Microsoft personal (anteriormente, cuenta de Live):
Integración de Azure AD V2 en una aplicación web ASP.NET Core 2.0:
Vea este vídeo asociado.
Llamada a una API web ASP.NET Core 2.0 desde una aplicación de WPF con Azure AD V2:
Vea este vídeo asociado.

Ejemplo de Azure AD B2C


En este ejemplo se muestra cómo integrar Azure AD B2C, lo que permite a los usuarios iniciar sesión con
identidades sociales, como Facebook y Google, entre otros.
Una aplicación API web de ASP.NET Core con Azure AD B2C
Autenticación en la nube con Azure Active Directory
B2C en ASP.NET Core
22/06/2018 • 10 minutes to read • Edit Online

Por Cam Soper


Azure B2C Directory Active (Azure AD B2C ) es una solución de administración de identidades de nube para
aplicaciones móviles y web. El servicio proporciona autenticación para las aplicaciones hospedadas en la nube y
locales. Tipos de autenticación incluyen cuentas individuales, las cuentas de red social y federado cuentas
empresariales. Además, Azure AD B2C puede proporcionar la autenticación multifactor con una configuración
mínima.

TIP
Azure Active Directory (Azure AD) Azure AD B2C son ofertas de productos independientes. Un inquilino de Azure AD
representa una organización, mientras que un inquilino de Azure AD B2C representa una colección de identidades para su
uso con aplicaciones de usuario de confianza. Para obtener más información, consulte Azure AD B2C: preguntas más
frecuentes (P+F).

En este tutorial, aprenderá cómo:


Crear a un inquilino de Azure Active Directory B2C
Registrar una aplicación en Azure AD B2C
Usar Visual Studio para crear una aplicación web de ASP.NET Core configurada para usar al inquilino de Azure
AD B2C para la autenticación
Configurar directivas que controlan el comportamiento del inquilino de Azure AD B2C

Requisitos previos
Se requiere para este tutorial lo siguiente:
Suscripción de Microsoft Azure
Visual Studio de 2017 (cualquier edición)

Crear al inquilino de Azure Active Directory B2C


Crear un inquilino de Azure Active Directory B2C tal como se describe en la documentación de. Cuando se le
solicite, asociar al inquilino con una suscripción de Azure es opcional para este tutorial.

Registrar la aplicación en Azure AD B2C


En el inquilino de Azure AD B2C recién creado, registrar la aplicación con los pasos descritos en la documentación
de en el registrar una aplicación web sección. Detener en el crear un secreto de cliente de aplicación web
sección. Un secreto de cliente no es necesario para este tutorial.
Utilice los siguientes valores:
PARÁMETRO VALOR NOTAS

Name <Nombre de la aplicación> Escriba un nombre para la aplicación


que describe la aplicación a los
consumidores.

Incluir la aplicación web / web API Sí

Permitir flujo implícito Sí

Dirección URL de respuesta https://localhost:44300 Direcciones URL de respuesta son los


puntos de conexión que Azure AD B2C
devuelve los tokens que solicita la
aplicación. Visual Studio proporciona la
dirección URL de respuesta a la utilice.
Por ahora, escriba
https://localhost:44300 para
completar el formulario.

URI del Id. de aplicación Deje en blanco No se necesita para este tutorial.

Incluir a cliente nativo No

WARNING
Si la configuración de una URL de respuesta no localhost, tener en cuenta el restricciones en lo que se permite en la lista
dirección URL de respuesta.

Una vez registrada la aplicación, se muestra la lista de aplicaciones en el inquilino. Seleccione la aplicación que se
acaba de registrar. Seleccione el copia icono a la derecha de la Id. de aplicación campo para copiarlo en el
Portapapeles.
No hay nada más pueden configurarse en el inquilino de Azure AD B2C en este momento, pero deje abierta la
ventana del explorador. Hay una mayor configuración después de crea la aplicación de ASP.NET Core.

Crear una aplicación básica de ASP.NET en Visual Studio de 2017


La plantilla de aplicación Web de Visual Studio puede configurarse para usar al inquilino de Azure AD B2C para la
autenticación.
En Visual Studio:
1. Cree una aplicación web de ASP.NET Core.
2. Seleccione aplicación Web en la lista de plantillas.
3. Seleccione el Cambiar autenticación botón.
4. En el Cambiar autenticación cuadro de diálogo, seleccione cuentas de usuario individualesy, a
continuación, seleccione conectar a un almacén de usuario existente en la nube en la lista desplegable.

5. Rellene el formulario con los siguientes valores:

PARÁMETRO VALOR

Nombre de dominio <el nombre de dominio del inquilino B2C>

Identificador de la aplicación <Pegue el identificador de aplicación desde el


Portapapeles>
PARÁMETRO VALOR

Ruta de acceso de devolución de llamada <usar el valor predeterminado>

Directiva de inicio de sesión o inicio de sesión B2C_1_SiUpIn

Directiva de restablecimiento de contraseña B2C_1_SSPR

Editar directiva de perfil <Deje en blanco>

Seleccione el copia vínculo junto a URI de respuesta para copiar el URI de respuesta en el Portapapeles.
Seleccione Aceptar para cerrar el Cambiar autenticación cuadro de diálogo. Seleccione Aceptar para
crear la aplicación web.

Finalizar el registro de aplicación B2C


Volver a la ventana del explorador con las propiedades de aplicación B2C permanece abiertas. Cambiar la
contraseña dirección URL de respuesta especificado anteriormente para el valor copiado desde Visual Studio.
Seleccione guardar en la parte superior de la ventana.

TIP
Si no copia la dirección URL de respuesta, use la dirección SSL desde la pestaña de depuración en las propiedades del
proyecto web y anexar la CallbackPath valor de appSettings.JSON que se.

Configurar directivas
Siga los pasos de la documentación de Azure AD B2C a crear una directiva de inicio de sesión o inicio de sesióny,
a continuación, crear una directiva de restablecimiento de contraseña. Use los valores de ejemplo proporcionados
en la documentación de proveedores de identidades, atributos suscripción, y notificaciones de la
aplicación. Mediante el ejecutar ahora botón para probar las directivas, como se describe en la documentación
es opcional.

WARNING
Asegúrese de que los nombres de directiva son exactamente como se describe en la documentación de esas directivas se
usaron en el Cambiar autenticación cuadro de diálogo de Visual Studio. Los nombres de directiva se pueden comprobar en
appSettings.JSON que se.

Ejecutar la aplicación
En Visual Studio, presione F5 para compilar y ejecutar la aplicación. Después de que se inicie la aplicación web,
seleccione iniciar sesión en.
El explorador se redirige a los inquilinos de Azure AD B2C. Inicie sesión con una cuenta existente (si se ha creado
uno probar las directivas) o seleccione Regístrese ahora para crear una nueva cuenta. El ¿olvidó su contraseña?
vínculo se usa para restablecer una contraseña olvidada.
Después de iniciar sesión correctamente, el explorador se redirige a la aplicación web.

Pasos siguientes
En este tutorial ha aprendido a:
Crear a un inquilino de Azure Active Directory B2C
Registrar una aplicación en Azure AD B2C
Usar Visual Studio para crear una aplicación de ASP.NET Core Web configurado para usar al inquilino de
Azure AD B2C para la autenticación
Configurar directivas que controlan el comportamiento del inquilino de Azure AD B2C
Ahora que la aplicación de ASP.NET Core está configurada para usar Azure AD B2C para la autenticación, el
atributo Authorize puede usarse para proteger la aplicación. Continuar desarrollando la aplicación por el
aprendizaje para:
Personalizar la interfaz de usuario de Azure AD B2C.
Configurar requisitos de complejidad de contraseña.
Habilitar la autenticación multifactor.
Configurar proveedores de identidades adicional, como Microsoft, Facebook, Google, Amazon, de Twitter y
otros.
Usar la API de Azure AD Graph para recuperar información de usuario adicional, como la pertenencia a
grupos, desde el inquilino de Azure AD B2C.
Proteger un núcleo de ASP.NET web API con Azure AD B2C.
Llamar a una API web de .NET desde una aplicación web de .NET con Azure AD B2C.
Autenticación de nube en web API con Azure Active
Directory B2C en ASP.NET Core
23/06/2018 • 16 minutes to read • Edit Online

Por Cam Soper


Azure B2C Directory Active (Azure AD B2C ) es una solución de administración de identidades de nube para
aplicaciones móviles y web. El servicio proporciona autenticación para las aplicaciones hospedadas en la nube y
locales. Tipos de autenticación incluyen cuentas individuales, las cuentas de red social y federado cuentas
empresariales. Además, Azure AD B2C puede proporcionar la autenticación multifactor con una configuración
mínima.

TIP
Azure Active Directory (Azure AD) y Azure AD B2C son ofertas de producto independiente. Un inquilino de Azure AD
representa una organización, mientras que un inquilino de Azure AD B2C representa una colección de identidades para su
uso con aplicaciones de usuario de confianza. Para obtener más información, consulte Azure AD B2C: preguntas más
frecuentes (P+F).

Puesto que las API web no tienen ninguna interfaz de usuario, se trata de no se puede redirigir al usuario a un
servicio de token seguro como Azure AD B2C. En su lugar, la API se pasa un token de portador de la aplicación
que realiza la llamada, que ya se ha autenticado al usuario con Azure AD B2C. La API, a continuación, valida el
token sin interacción directa del usuario.
En este tutorial, aprenderá cómo:
Crear a un inquilino de Azure Active Directory B2C.
Registrar una API Web en Azure AD B2C.
Usar Visual Studio para crear una API Web configurado para usar al inquilino de Azure AD B2C para la
autenticación.
Configurar directivas que controlan el comportamiento del inquilino de Azure AD B2C.
Use Postman para simular una aplicación web que presenta un cuadro de diálogo de inicio de sesión, recupera
un token y lo utiliza para realizar una solicitud en la API web.

Requisitos previos
Se requiere para este tutorial lo siguiente:
Suscripción de Microsoft Azure
Visual Studio de 2017 (cualquier edición)
Postman

Crear al inquilino de Azure Active Directory B2C


Crear un inquilino de Azure AD B2C tal como se describe en la documentación de. Cuando se le solicite, asociar al
inquilino con una suscripción de Azure es opcional para este tutorial.

Configurar una directiva de inicio de sesión o inicio de sesión


Siga los pasos de la documentación de Azure AD B2C a crear una directiva de inicio de sesión o inicio de sesión.
Nombre de la directiva SiUpIn. Use los valores de ejemplo proporcionados en la documentación de proveedores
de identidades, atributos suscripción, y notificaciones de la aplicación. Mediante el ejecutar ahora botón
para probar la directiva como se describe en la documentación es opcional.

Registrar la API en Azure AD B2C


En el inquilino de Azure AD B2C recién creado, registrar la API mediante los pasos descritos en la documentación
de en el registrar una API web sección.
Utilice los siguientes valores:

PARÁMETRO VALOR NOTAS

Name <Nombre de la API> Escriba un nombre para la aplicación


que describe la aplicación a los
consumidores.

Incluir la aplicación web / web API Sí

Permitir flujo implícito Sí

Dirección URL de respuesta https://localhost Direcciones URL de respuesta son los


puntos de conexión que Azure AD B2C
devuelve los tokens que solicita la
aplicación.

URI del Id. de aplicación API El URI no tiene que resolverse en una
dirección física. Solo debe ser único.

Incluir a cliente nativo No

Una vez registrada la API, se muestra la lista de aplicaciones y las API en el inquilino. Seleccione la API que se
acaba de registrar. Seleccione el copia icono a la derecha de la Id. de aplicación campo para copiarlo en el
Portapapeles. Seleccione publicado ámbitos y compruebe el valor predeterminado user_impersonation ámbito
está presente.

Crear una aplicación básica de ASP.NET en Visual Studio de 2017


La plantilla de aplicación Web de Visual Studio puede configurarse para usar al inquilino de Azure AD B2C para la
autenticación.
En Visual Studio:
1. Cree una aplicación web de ASP.NET Core.
2. Seleccione API Web en la lista de plantillas.
3. Seleccione el Cambiar autenticación botón.
4. En el Cambiar autenticación cuadro de diálogo, seleccione cuentas de usuario individualesy, a
continuación, seleccione conectar a un almacén de usuario existente en la nube en la lista desplegable.

5. Rellene el formulario con los siguientes valores:

PARÁMETRO VALOR

Nombre de dominio <el nombre de dominio del inquilino B2C>

Identificador de la aplicación <Pegue el identificador de aplicación desde el


Portapapeles>

Directiva de inicio de sesión o inicio de sesión B2C_1_SiUpIn

Seleccione Aceptar para cerrar el Cambiar autenticación cuadro de diálogo. Seleccione Aceptar para
crear la aplicación web.
Visual Studio crea la API web con un controlador denominado ValuesController.cs que devuelva valores
codificados de forma rígida para las solicitudes GET. La clase se decora con el atributo Authorize, por lo que todas
las solicitudes requieren autenticación.

Ejecute la API web


En Visual Studio, ejecute la API. Visual Studio inicia un explorador que apunta en dirección URL raíz de la API.
Tenga en cuenta la dirección URL en la barra de direcciones y dejar la API que se ejecuta en segundo plano.

NOTE
Puesto que no hay ningún controlador definido para la dirección URL raíz, el explorador muestra un error 404 de (página no
encontrada). Este es el comportamiento normal.

Utilice a Postman para obtener un token y la API de pruebas


Postman es una herramienta para probar las API web. Para este tutorial, Postman simula una aplicación web que
tiene acceso a la API web en nombre del usuario.
Registrar a Postman como una aplicación web
Puesto que Postman simula una aplicación web que puede obtener tokens de los inquilinos de Azure AD B2C, se
debe registrar en el inquilino como una aplicación web. Registrar Postman con los pasos descritos en la
documentación de en el registrar una aplicación web sección. Detener en el crear un secreto de cliente de
aplicación web sección. Un secreto de cliente no es necesario para este tutorial.
Utilice los siguientes valores:

PARÁMETRO VALOR NOTAS

Name Postman

Incluir la aplicación web / web API Sí

Permitir flujo implícito Sí

Dirección URL de respuesta https://getpostman.com/postman

URI del Id. de aplicación <Deje en blanco> No se necesita para este tutorial.

Incluir a cliente nativo No

La aplicación web recién registrado necesita permiso para tener acceso a la API web en nombre del usuario.
1. Seleccione Postman en la lista de aplicaciones y, a continuación, seleccione el acceso de API en el menú de la
izquierda.
2. Seleccione + agregar.
3. En el API seleccione de lista desplegable, seleccione el nombre de la API web.
4. En el seleccionar ámbitos de lista desplegable, asegúrese de que se seleccionan todos los ámbitos.
5. Seleccione Aceptar.
Tenga en cuenta Id. la aplicación Postman de aplicación, tal y como lo necesario para obtener un token de
portador.
Crear una solicitud de Postman
Inicie a Postman. De forma predeterminada, se muestra Postman el crear nuevo tras iniciar el cuadro de diálogo.
Si no se muestra el cuadro de diálogo, seleccione la + nuevo botón en la parte superior izquierda.
Desde el crear nuevo cuadro de diálogo:
1. Seleccione solicitar.

2. Escriba obtener valores en el nombre de la solicitud cuadro.


3. Seleccione + Crear colección para crear una nueva colección para almacenar la solicitud. Asignar nombre
a la colección tutoriales de ASP.NET Core y, a continuación, seleccione la marca de verificación.
4. Seleccione el guardar a los tutoriales de ASP.NET Core botón.
Probar la API web sin autenticación
Para comprobar que la API web requiere autenticación, primero hay que realizar una solicitud sin autenticación.
1. En el escriba la dirección URL de solicitud cuadro, escriba la dirección URL de ValuesController . La
dirección URL es el mismo valor que se muestra en el explorador con api/valores anexado. Un ejemplo
sería https://localhost:44375/api/values .
2. Seleccione el enviar botón.
3. Tenga en cuenta el estado de la respuesta es 401 no autorizado.
Obtener un token de portador
Para realizar una solicitud autenticada a la API web, se requiere un token de portador. Postman facilita la inicie
sesión en el inquilino de Azure AD B2C y obtener un token.
1. En el autorización ficha la tipo lista desplegable, seleccione OAuth 2.0. En el agregar datos de
autorización a lista desplegable, seleccione encabezados de solicitud. Seleccione obtener Token de
acceso nuevo.

2. Completar la obtener TOKEN de acceso nuevo diálogo como se indica a continuación:

PARÁMETRO VALOR NOTAS

Nombre del token <nombre del token> Escriba un nombre descriptivo para el
token.

Tipo de concesión Implícitas

Dirección URL de devolución de https://getpostman.com/postman


llamada

Dirección URL de autenticación Reemplace


https://login.microsoftonline.com/tfp/<tenant
domain <nombre_de_dominio_de> con el
name>/B2C_1_SiUpIn/oauth2/v2.0/authorize
nombre de dominio del inquilino.

Id. de cliente <Escriba la aplicación Postman Id.


de aplicación>
PARÁMETRO VALOR NOTAS

Secreto del cliente <Deje en blanco>

Ámbito https://<tenant domain Reemplace


name>/<api>/user_impersonation <nombre_de_dominio_de> con el
openid offline_access
nombre de dominio del inquilino.
Reemplace <api> con el nombre del
proyecto Web API. También puede
usar el identificador de aplicación. El
patrón para la dirección URL es:
https:// {tenant }.onmicrosoft.com/ {ap
p_name_or_id}/ {scope nombre}.

Autenticación de cliente Enviar las credenciales del cliente en


el cuerpo

3. Seleccione el solicitar Token botón.


4. Postman abre una nueva ventana que contiene el inicio de sesión del inquilino de Azure AD B2C en cuadro
de diálogo. Inicie sesión con una cuenta existente (si se ha creado uno probar las directivas) o seleccione
Regístrese ahora para crear una nueva cuenta. El ¿olvidó su contraseña? vínculo se usa para restablecer
una contraseña olvidada.
5. Después de iniciar sesión correctamente, la ventana se cierra y la administrar TOKENS de acceso
aparece el cuadro de diálogo. Desplácese hacia abajo hasta la parte inferior y seleccione el uso Token
botón.

Probar la API web con autenticación


Seleccione el enviar botón volver a enviar la solicitud. En esta ocasión, el estado de la respuesta es 200 Aceptar y
la carga de JSON es visible en la respuesta cuerpo ficha.
Pasos siguientes
En este tutorial ha aprendido a:
Crear a un inquilino de Azure Active Directory B2C.
Registrar una API Web en Azure AD B2C.
Usar Visual Studio para crear una API Web configurado para usar al inquilino de Azure AD B2C para la
autenticación.
Configurar directivas que controlan el comportamiento del inquilino de Azure AD B2C.
Use Postman para simular una aplicación web que presenta un cuadro de diálogo de inicio de sesión, recupera
un token y lo utiliza para realizar una solicitud en la API web.
Continuar desarrollando su API por el aprendizaje para:
Proteger un ASP.NET Core aplicación web con Azure AD B2C.
Llamar a una API web de .NET desde una aplicación web de .NET con Azure AD B2C.
Personalizar la interfaz de usuario de Azure AD B2C.
Configurar requisitos de complejidad de contraseña.
Habilitar la autenticación multifactor.
Configurar proveedores de identidades adicional, como Microsoft, Facebook, Google, Amazon, de Twitter y
otros.
Usar la API de Azure AD Graph para recuperar información de usuario adicional, como la pertenencia a
grupos, desde el inquilino de Azure AD B2C.
Artículos basados en los proyectos de ASP.NET Core
creados con cuentas de usuario individuales
22/06/2018 • 2 minutes to read • Edit Online

Identidad de ASP.NET Core se incluye en las plantillas de proyecto en Visual Studio con la opción de "Cuentas de
usuario individuales".
Las plantillas de autenticación están disponibles en .NET Core CLI con -au Individual :

dotnet new mvc -au Individual


dotnet new webapi -au Individual
dotnet new webapp -au Individual

NOTE
In ASP.NET Core 2.1 or later, webapp is an alias of the razor argument. If the dotnet new webapp <OPTIONS> command
loads the dotnet new command help instead of creating a new Razor Pages app, install the .NET Core 2.1 SDK.

dotnet new mvc -au Individual


dotnet new webapi -au Individual
dotnet new razor -au Individual

Los artículos siguientes muestran cómo usar el código generado en las plantillas de ASP.NET Core que utilizan
cuentas de usuario individuales:
Autenticación en dos fases con SMS
Confirmación de las cuentas y recuperación de contraseñas en ASP.NET Core
Crear una aplicación de ASP.NET Core con datos de usuario protegidos por autorización
Autorización en ASP.NET Core
19/06/2018 • 2 minutes to read • Edit Online

Introducción
Creación de una aplicación con datos de usuario protegidos por autorización
Autorización de páginas de Razor
Autorización simple
Autorización basada en roles
Autorización basada en notificaciones
Autorización basada en directivas
Proveedores de directivas de autorización personalizada
Inserción de dependencias en controladores de requisitos
Autorización basada en recursos
Autorización basada en visualizaciones
Authorize with a specific scheme (Autorización con un esquema específico)
Introducción a la autorización en ASP.NET Core
22/06/2018 • 2 minutes to read • Edit Online

La autorización se refiere al proceso que determina lo que un usuario es capaz de hacer. Por ejemplo, un usuario
administrativo puede crear una biblioteca de documentos, agregar documentos, editar documentos y eliminarlos.
Un usuario sin derechos administrativos que trabaje con la biblioteca solo está autorizado a leer los documentos.
La autorización es ortogonal y es independiente de la autenticación. Sin embargo, la autorización requiere un
mecanismo de autenticación. La autenticación es el proceso de determinar quién es un usuario. La autenticación
puede crear una o varias identidades para el usuario actual.

Tipos de autorización
Autorización de ASP.NET Core proporcionan una forma simple, declarativa rol y un variado basada en directivas
modelo. Autorización se expresa en los requisitos y controladores evaluación notificaciones de usuario con los
requisitos. Las comprobaciones imperativas pueden basarse en directivas simples ni que evaluar la identidad del
usuario y propiedades del recurso al que el usuario está intentando tener acceso.

Espacios de nombres
Componentes de autorización, incluido el AuthorizeAttribute y AllowAnonymousAttribute atributos, se encuentran
en el Microsoft.AspNetCore.Authorization espacio de nombres.
Consulte la documentación en autorización sencilla.
Crear una aplicación de ASP.NET Core con datos de
usuario protegidos por autorización
22/06/2018 • 29 minutes to read • Edit Online

Por Rick Anderson y Joe Audette


Este tutorial muestra cómo crear una aplicación web de ASP.NET Core con datos de usuario protegidos mediante
autorización. Muestra una lista de contactos que autentica los usuarios (registrados) ha creado. Hay tres grupos
de seguridad:
Usuarios registrados puede ver todos los datos aprobados y editar puede/eliminar sus propios datos.
Administradores de puede aprobar o rechazar los datos de contacto. Sólo los contactos aprobados son
visibles para los usuarios.
Los administradores puede aprobar o rechazar y editar o eliminar los datos.
En la siguiente imagen, usuario Rick ( rick@example.com ) ha iniciado sesión. Rick solo puede ver los contactos
aprobados y editar/eliminar/crear nuevo vínculos de sus contactos. El último registro, creado por Rick, muestra
editar y eliminar vínculos. Otros usuarios no verán el último registro hasta que un administrador o un
administrador cambia el estado a "Aprobado".

En la siguiente imagen, manager@contoso.com está firmado en y en el rol de administrador:


La siguiente imagen muestra a los administradores de la vista de detalles de un contacto:

El aprobar y rechazar solo se muestran los botones para los administradores y administradores.
En la siguiente imagen, admin@contoso.com está firmado en y en la función Administradores:
El administrador tiene todos los privilegios. Puede leer, editar o eliminar cualquier contacto y cambiar el estado de
contactos.
La aplicación se creó con scaffolding siguiente Contact modelo:

public class Contact


{
public int ContactId { get; set; }
public string Name { get; set; }
public string Address { get; set; }
public string City { get; set; }
public string State { get; set; }
public string Zip { get; set; }
[DataType(DataType.EmailAddress)]
public string Email { get; set; }
}

El ejemplo contiene los siguientes controladores de autorización:


ContactIsOwnerAuthorizationHandler : Se asegura de que un usuario solo puede editar sus datos.
ContactManagerAuthorizationHandler : Permite a los administradores aprobar o rechazar los contactos.
ContactAdministratorsAuthorizationHandler : Permite a los administradores para aprobar o rechazar los
contactos y editar o eliminar contactos.

Requisitos previos
Este tutorial se avanza. Debe estar familiarizado con:
ASP.NET Core
Autenticación
Confirmación de cuentas y recuperación de contraseñas
Autorización
Entity Framework Core
Vea este archivo PDF para la versión principal de ASP.NET MVC. La versión 1.1 de ASP.NET Core de este tutorial
está en esto carpeta. La 1.1 incluye el ejemplo de ASP.NET Core el ejemplos.

El inicio y la aplicación completada


Descargar el completado aplicación. Prueba la aplicación completada por lo que se familiarice con sus
características de seguridad.
La aplicación de inicio
Descargar el starter aplicación.
Ejecutar la aplicación, pulse el ContactManager vincular y compruebe que puede crear, editar y eliminar un
contacto.

Proteger los datos de usuario


Las siguientes secciones contienen todos los pasos principales para crear la aplicación de datos de usuario
seguras. Le resultará útil para hacer referencia al proyecto completado.
Asociar los datos de contacto para el usuario
Usar ASP.NET identidad Id. de usuario para garantizar que los usuarios puede editar sus datos, pero no otros
datos de los usuarios. Agregar OwnerID y ContactStatus a la Contact modelo:

public class Contact


{
public int ContactId { get; set; }

// user ID from AspNetUser table.


public string OwnerID { get; set; }

public string Name { get; set; }


public string Address { get; set; }
public string City { get; set; }
public string State { get; set; }
public string Zip { get; set; }
[DataType(DataType.EmailAddress)]
public string Email { get; set; }

public ContactStatus Status { get; set; }


}

public enum ContactStatus


{
Submitted,
Approved,
Rejected
}

OwnerID es el identificador del usuario desde el AspNetUser tabla el identidad base de datos. El Status campo
determina si un contacto es visible para los usuarios en general.
Crear una nueva migración y actualizar la base de datos:
dotnet ef migrations add userID_Status
dotnet ef database update

Requerir HTTPS y los usuarios autenticados


Agregar IHostingEnvironment a Startup :

public class Startup


{
public Startup(IConfiguration configuration, IHostingEnvironment env)
{
Configuration = configuration;
Environment = env;
}

public IConfiguration Configuration { get; }


private IHostingEnvironment Environment { get; }

En el ConfigureServices método de la Startup.cs , agregue el RequireHttpsAttribute filtro de autorización:

public void ConfigureServices(IServiceCollection services)


{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();

var skipHTTPS = Configuration.GetValue<bool>("LocalTest:skipHTTPS");


// requires using Microsoft.AspNetCore.Mvc;
services.Configure<MvcOptions>(options =>
{
// Set LocalTest:skipHTTPS to true to skip SSL requrement in
// debug mode. This is useful when not using Visual Studio.
if (Environment.IsDevelopment() && !skipHTTPS)
{
options.Filters.Add(new RequireHttpsAttribute());
}
});

Si está utilizando Visual Studio, habilitar HTTPS.


Para redirigir las solicitudes HTTP a HTTPS, consulte Middleware de reescritura de dirección URL. Si está usando
Visual Studio Code o pruebas en una plataforma local que no incluye un certificado de prueba para HTTPS:
Establecer "LocalTest:skipHTTPS": true en el appsettings. Developement.JSON archivo.
Requerir a los usuarios autenticados
Establecer la directiva de autenticación predeterminado para exigir que los usuarios se autentiquen. Puede
rechazar la autenticación en el nivel de método página Razor, controlador o acción con el [AllowAnonymous]
atributo. Establecer la directiva de autenticación predeterminado para exigir que los usuarios se autentiquen
protege recién agregado las páginas Razor y controladores. Existencia de autenticación que requiere de forma
predeterminada es más segura que confiar en los nuevos controladores y las páginas de Razor para incluir la
[Authorize] atributo.

Con el requisito de todos los usuarios autenticados, el AuthorizeFolder y AuthorizePage llamadas no son
necesarias.
Actualización ConfigureServices con los cambios siguientes:
Convertir en comentario AuthorizeFolder y AuthorizePage .
Establecer la directiva de autenticación predeterminado para exigir que los usuarios se autentiquen.

public void ConfigureServices(IServiceCollection services)


{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();

var skipHTTPS = Configuration.GetValue<bool>("LocalTest:skipHTTPS");


// requires using Microsoft.AspNetCore.Mvc;
services.Configure<MvcOptions>(options =>
{
// Set LocalTest:skipHTTPS to true to skip SSL requrement in
// debug mode. This is useful when not using Visual Studio.
if (Environment.IsDevelopment() && !skipHTTPS)
{
options.Filters.Add(new RequireHttpsAttribute());
}
});

services.AddMvc();
//.AddRazorPagesOptions(options =>
//{
// options.Conventions.AuthorizeFolder("/Account/Manage");
// options.Conventions.AuthorizePage("/Account/Logout");
//});

services.AddSingleton<IEmailSender, EmailSender>();

// requires: using Microsoft.AspNetCore.Authorization;


// using Microsoft.AspNetCore.Mvc.Authorization;
services.AddMvc(config =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
});

Agregar AllowAnonymous al índice, las páginas sobre y, a continuación, póngase en contacto con por lo que los
usuarios anónimos pueden recibir información sobre el sitio antes de que registrar.

// requires using Microsoft.AspNetCore.Mvc.RazorPages;


[AllowAnonymous]
public class IndexModel : PageModel
{
public void OnGet()
{

}
}

Agregar [AllowAnonymous] a la LoginModel y RegisterModel.


Configurar la cuenta de prueba
La SeedData clase crea dos cuentas: administrador y el administrador. Use la herramienta secreto administrador
para establecer una contraseña para estas cuentas. Establecer la contraseña desde el directorio del proyecto (el
directorio que contiene Program.cs):
dotnet user-secrets set SeedUserPW <PW>

Si no utiliza una contraseña segura, se produce una excepción cuando SeedData.Initialize se llama.
Actualización Main usar la contraseña de prueba:

public class Program


{
public static void Main(string[] args)
{
var host = BuildWebHost(args);

using (var scope = host.Services.CreateScope())


{
var services = scope.ServiceProvider;
var context = services.GetRequiredService<ApplicationDbContext>();
context.Database.Migrate();

// requires using Microsoft.Extensions.Configuration;


var config = host.Services.GetRequiredService<IConfiguration>();
// Set password with the Secret Manager tool.
// dotnet user-secrets set SeedUserPW <pw>

var testUserPw = config["SeedUserPW"];

try
{
SeedData.Initialize(services, testUserPw).Wait();
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred while seeding the database.");
throw ex;
}
}

host.Run();
}

public static IWebHost BuildWebHost(string[] args) =>


WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.Build();
}

Crear las cuentas de prueba y actualizar los contactos


Actualización de la Initialize método en la SeedData clase para crear las cuentas de prueba:
public static async Task Initialize(IServiceProvider serviceProvider, string testUserPw)
{
using (var context = new ApplicationDbContext(
serviceProvider.GetRequiredService<DbContextOptions<ApplicationDbContext>>()))
{
// For sample purposes we are seeding 2 users both with the same password.
// The password is set with the following command:
// dotnet user-secrets set SeedUserPW <pw>
// The admin user can do anything

var adminID = await EnsureUser(serviceProvider, testUserPw, "admin@contoso.com");


await EnsureRole(serviceProvider, adminID, Constants.ContactAdministratorsRole);

// allowed user can create and edit contacts that they create
var uid = await EnsureUser(serviceProvider, testUserPw, "manager@contoso.com");
await EnsureRole(serviceProvider, uid, Constants.ContactManagersRole);

SeedDB(context, adminID);
}
}

private static async Task<string> EnsureUser(IServiceProvider serviceProvider,


string testUserPw, string UserName)
{
var userManager = serviceProvider.GetService<UserManager<ApplicationUser>>();

var user = await userManager.FindByNameAsync(UserName);


if (user == null)
{
user = new ApplicationUser { UserName = UserName };
await userManager.CreateAsync(user, testUserPw);
}

return user.Id;
}

private static async Task<IdentityResult> EnsureRole(IServiceProvider serviceProvider,


string uid, string role)
{
IdentityResult IR = null;
var roleManager = serviceProvider.GetService<RoleManager<IdentityRole>>();

if (!await roleManager.RoleExistsAsync(role))
{
IR = await roleManager.CreateAsync(new IdentityRole(role));
}

var userManager = serviceProvider.GetService<UserManager<ApplicationUser>>();

var user = await userManager.FindByIdAsync(uid);

IR = await userManager.AddToRoleAsync(user, role);

return IR;
}

Agregue el identificador de usuario de administrador y ContactStatus a los contactos. Realice uno de los
contactos "Enviado" y un "rechazada". Agregue el Id. de usuario y el estado para todos los contactos. Póngase en
contacto un solo con se muestra:
public static void SeedDB(ApplicationDbContext context, string adminID)
{
if (context.Contact.Any())
{
return; // DB has been seeded
}

context.Contact.AddRange(
new Contact
{
Name = "Debra Garcia",
Address = "1234 Main St",
City = "Redmond",
State = "WA",
Zip = "10999",
Email = "debra@example.com",
Status = ContactStatus.Approved,
OwnerID = adminID
},

Crear propietario, el administrador y los controladores de autorización


de administrador
Crear un clase en el autorización carpeta. El
ContactIsOwnerAuthorizationHandler
ContactIsOwnerAuthorizationHandler comprueba que el usuario que actúa en un recurso posee el recurso.
using System.Threading.Tasks;
using ContactManager.Data;
using ContactManager.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using Microsoft.AspNetCore.Identity;

namespace ContactManager.Authorization
{
public class ContactIsOwnerAuthorizationHandler
: AuthorizationHandler<OperationAuthorizationRequirement, Contact>
{
UserManager<ApplicationUser> _userManager;

public ContactIsOwnerAuthorizationHandler(UserManager<ApplicationUser>
userManager)
{
_userManager = userManager;
}

protected override Task


HandleRequirementAsync(AuthorizationHandlerContext context,
OperationAuthorizationRequirement requirement,
Contact resource)
{
if (context.User == null || resource == null)
{
// Return Task.FromResult(0) if targeting a version of
// .NET Framework older than 4.6:
return Task.CompletedTask;
}

// If we're not asking for CRUD permission, return.

if (requirement.Name != Constants.CreateOperationName &&


requirement.Name != Constants.ReadOperationName &&
requirement.Name != Constants.UpdateOperationName &&
requirement.Name != Constants.DeleteOperationName )
{
return Task.CompletedTask;
}

if (resource.OwnerID == _userManager.GetUserId(context.User))
{
context.Succeed(requirement);
}

return Task.CompletedTask;
}
}
}

El ContactIsOwnerAuthorizationHandler llamadas contexto. Correctamente si el usuario autenticado actual es el


propietario del contacto. Controladores de autorización general:
Devolver context.Succeed cuando se cumplen los requisitos.
Devolver Task.CompletedTask cuando no se cumplen los requisitos. Task.CompletedTask no correcto o error—
permite que otros controladores de autorización para que se ejecute.
Si necesita explícitamente un error, devolver contexto. Un error.
La aplicación permite a los propietarios de contacto para editar, eliminar o crear sus propios datos.
ContactIsOwnerAuthorizationHandler no tiene que comprobar la operación pasada en el parámetro de requisito.
Crear un controlador del Administrador de autorización
Crear un ContactManagerAuthorizationHandler clase en el autorización carpeta. El
ContactManagerAuthorizationHandler comprueba que el usuario que actúa en el recurso es un administrador. Solo
los administradores pueden aprobar o rechazar los cambios de contenido (nuevos o modificados).

using System.Threading.Tasks;
using ContactManager.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using Microsoft.AspNetCore.Identity;

namespace ContactManager.Authorization
{
public class ContactManagerAuthorizationHandler :
AuthorizationHandler<OperationAuthorizationRequirement, Contact>
{
protected override Task
HandleRequirementAsync(AuthorizationHandlerContext context,
OperationAuthorizationRequirement requirement,
Contact resource)
{
if (context.User == null || resource == null)
{
return Task.CompletedTask;
}

// If not asking for approval/reject, return.


if (requirement.Name != Constants.ApproveOperationName &&
requirement.Name != Constants.RejectOperationName)
{
return Task.CompletedTask;
}

// Managers can approve or reject.


if (context.User.IsInRole(Constants.ContactManagersRole))
{
context.Succeed(requirement);
}

return Task.CompletedTask;
}
}
}

Crear un controlador de autorización de administrador


Crear un ContactAdministratorsAuthorizationHandler clase en el autorización carpeta. El
ContactAdministratorsAuthorizationHandler comprueba que el usuario que actúa en el recurso es un
administrador. Administrador puede hacer que todas las operaciones.
using System.Threading.Tasks;
using ContactManager.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;

namespace ContactManager.Authorization
{
public class ContactAdministratorsAuthorizationHandler
: AuthorizationHandler<OperationAuthorizationRequirement, Contact>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
OperationAuthorizationRequirement requirement,
Contact resource)
{
if (context.User == null)
{
return Task.CompletedTask;
}

// Administrators can do anything.


if (context.User.IsInRole(Constants.ContactAdministratorsRole))
{
context.Succeed(requirement);
}

return Task.CompletedTask;
}
}
}

Registrar los controladores de autorización


Servicios mediante Entity Framework Core deben estar registrados para inyección de dependencia con
AddScoped. El ContactIsOwnerAuthorizationHandler usa ASP.NET Core identidad, que se basa en Entity
Framework Core. Registre los controladores con la colección de servicio para que estén disponibles para la
ContactsController a través de inyección de dependencia. Agregue el código siguiente al final de
ConfigureServices :
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();

var skipHTTPS = Configuration.GetValue<bool>("LocalTest:skipHTTPS");


// requires using Microsoft.AspNetCore.Mvc;
services.Configure<MvcOptions>(options =>
{
// Set LocalTest:skipHTTPS to true to skip SSL requrement in
// debug mode. This is useful when not using Visual Studio.
if (Environment.IsDevelopment() && !skipHTTPS)
{
options.Filters.Add(new RequireHttpsAttribute());
}
});

services.AddMvc();
//.AddRazorPagesOptions(options =>
//{
// options.Conventions.AuthorizeFolder("/Account/Manage");
// options.Conventions.AuthorizePage("/Account/Logout");
//});

services.AddSingleton<IEmailSender, EmailSender>();

// requires: using Microsoft.AspNetCore.Authorization;


// using Microsoft.AspNetCore.Mvc.Authorization;
services.AddMvc(config =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
});

// Authorization handlers.
services.AddScoped<IAuthorizationHandler,
ContactIsOwnerAuthorizationHandler>();

services.AddSingleton<IAuthorizationHandler,
ContactAdministratorsAuthorizationHandler>();

services.AddSingleton<IAuthorizationHandler,
ContactManagerAuthorizationHandler>();
}

ContactAdministratorsAuthorizationHandler y ContactManagerAuthorizationHandler se agregan como singleton.


Son singletons porque no usan EF y toda la información necesaria se encuentra en la Context parámetro de la
HandleRequirementAsync método.

Admitir la autorización
En esta sección, actualice las páginas Razor y agregar una clase de requisitos de las operaciones.
Revisión de la clase de requisitos de operaciones de contacto
Revise la ContactOperations clase. Esta clase contiene los requisitos de la aplicación admite:
using Microsoft.AspNetCore.Authorization.Infrastructure;

namespace ContactManager.Authorization
{
public static class ContactOperations
{
public static OperationAuthorizationRequirement Create =
new OperationAuthorizationRequirement {Name=Constants.CreateOperationName};
public static OperationAuthorizationRequirement Read =
new OperationAuthorizationRequirement {Name=Constants.ReadOperationName};
public static OperationAuthorizationRequirement Update =
new OperationAuthorizationRequirement {Name=Constants.UpdateOperationName};
public static OperationAuthorizationRequirement Delete =
new OperationAuthorizationRequirement {Name=Constants.DeleteOperationName};
public static OperationAuthorizationRequirement Approve =
new OperationAuthorizationRequirement {Name=Constants.ApproveOperationName};
public static OperationAuthorizationRequirement Reject =
new OperationAuthorizationRequirement {Name=Constants.RejectOperationName};
}

public class Constants


{
public static readonly string CreateOperationName = "Create";
public static readonly string ReadOperationName = "Read";
public static readonly string UpdateOperationName = "Update";
public static readonly string DeleteOperationName = "Delete";
public static readonly string ApproveOperationName = "Approve";
public static readonly string RejectOperationName = "Reject";

public static readonly string ContactAdministratorsRole =


"ContactAdministrators";
public static readonly string ContactManagersRole = "ContactManagers";
}
}

Crear una clase base para las páginas de Razor


Cree una clase base que contiene los servicios usados en los contactos de las páginas de Razor. La clase base
coloca ese código de inicialización en una ubicación:

using ContactManager.Data;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace ContactManager.Pages.Contacts
{
public class DI_BasePageModel : PageModel
{
protected ApplicationDbContext Context { get; }
protected IAuthorizationService AuthorizationService { get; }
protected UserManager<ApplicationUser> UserManager { get; }

public DI_BasePageModel(
ApplicationDbContext context,
IAuthorizationService authorizationService,
UserManager<ApplicationUser> userManager) : base()
{
Context = context;
UserManager = userManager;
AuthorizationService = authorizationService;
}
}
}
El código anterior:
Agrega el IAuthorizationService servicio para tener acceso a los controladores de autorización.
Agrega la identidad UserManager servicio.
Agregue la ApplicationDbContext .
Actualizar el CreateModel
Actualizar el constructor de modelo de página de create para usar la DI_BasePageModel clase base:

public class CreateModel : DI_BasePageModel


{
public CreateModel(
ApplicationDbContext context,
IAuthorizationService authorizationService,
UserManager<ApplicationUser> userManager)
: base(context, authorizationService, userManager)
{
}

Actualización de la CreateModel.OnPostAsync método:


Agregar el identificador de usuario para el Contact modelo.
Llamar al controlador de autorización para comprobar que el usuario tiene permiso para crear contactos.

public async Task<IActionResult> OnPostAsync()


{
if (!ModelState.IsValid)
{
return Page();
}

Contact.OwnerID = UserManager.GetUserId(User);

// requires using ContactManager.Authorization;


var isAuthorized = await AuthorizationService.AuthorizeAsync(
User, Contact,
ContactOperations.Create);
if (!isAuthorized.Succeeded)
{
return new ChallengeResult();
}

Context.Contact.Add(Contact);
await Context.SaveChangesAsync();

return RedirectToPage("./Index");
}

Actualizar la IndexModel
Actualización de la OnGetAsync método por lo que sólo los contactos aprobados se muestran a los usuarios en
general:
public class IndexModel : DI_BasePageModel
{
public IndexModel(
ApplicationDbContext context,
IAuthorizationService authorizationService,
UserManager<ApplicationUser> userManager)
: base(context, authorizationService, userManager)
{
}

public IList<Contact> Contact { get; set; }

public async Task OnGetAsync()


{
var contacts = from c in Context.Contact
select c;

var isAuthorized = User.IsInRole(Constants.ContactManagersRole) ||


User.IsInRole(Constants.ContactAdministratorsRole);

var currentUserId = UserManager.GetUserId(User);

// Only approved contacts are shown UNLESS you're authorized to see them
// or you are the owner.
if (!isAuthorized)
{
contacts = contacts.Where(c => c.Status == ContactStatus.Approved
|| c.OwnerID == currentUserId);
}

Contact = await contacts.ToListAsync();


}
}

Actualizar la EditModel
Agregue un controlador de autorización para comprobar que el usuario es propietario del contacto. Dado que se
está validando la autorización de recursos, el [Authorize] atributo no es suficiente. La aplicación no tiene acceso
al recurso cuando se evalúan los atributos. Autorización basada en recursos debe ser imperativo. Las
comprobaciones deben realizarse una vez que la aplicación tenga acceso al recurso, cargándolo en el modelo de
páginas o cargándolo en su propio controlador. Con frecuencia tener acceso al recurso pasando la clave de
recurso.

public class EditModel : DI_BasePageModel


{
public EditModel(
ApplicationDbContext context,
IAuthorizationService authorizationService,
UserManager<ApplicationUser> userManager)
: base(context, authorizationService, userManager)
{
}

[BindProperty]
public Contact Contact { get; set; }

public async Task<IActionResult> OnGetAsync(int id)


{
Contact = await Context.Contact.FirstOrDefaultAsync(
m => m.ContactId == id);

if (Contact == null)
{
return NotFound();
}
}

var isAuthorized = await AuthorizationService.AuthorizeAsync(


User, Contact,
ContactOperations.Update);
if (!isAuthorized.Succeeded)
{
return new ChallengeResult();
}

return Page();
}

public async Task<IActionResult> OnPostAsync(int id)


{
if (!ModelState.IsValid)
{
return Page();
}

// Fetch Contact from DB to get OwnerID.


var contact = await Context
.Contact.AsNoTracking()
.FirstOrDefaultAsync(m => m.ContactId == id);

if (contact == null)
{
return NotFound();
}

var isAuthorized = await AuthorizationService.AuthorizeAsync(


User, contact,
ContactOperations.Update);
if (!isAuthorized.Succeeded)
{
return new ChallengeResult();
}

Contact.OwnerID = contact.OwnerID;

Context.Attach(Contact).State = EntityState.Modified;

if (contact.Status == ContactStatus.Approved)
{
// If the contact is updated after approval,
// and the user cannot approve,
// set the status back to submitted so the update can be
// checked and approved.
var canApprove = await AuthorizationService.AuthorizeAsync(User,
contact,
ContactOperations.Approve);

if (!canApprove.Succeeded)
{
contact.Status = ContactStatus.Submitted;
}
}

await Context.SaveChangesAsync();

return RedirectToPage("./Index");
}

private bool ContactExists(int id)


{
return Context.Contact.Any(e => e.ContactId == id);
}
}
Actualizar la DeleteModel
Actualizar el modelo de páginas de delete para usar el controlador de autorización para comprobar que el usuario
tiene permiso delete en el contacto.

public class DeleteModel : DI_BasePageModel


{
public DeleteModel(
ApplicationDbContext context,
IAuthorizationService authorizationService,
UserManager<ApplicationUser> userManager)
: base(context, authorizationService, userManager)
{
}

[BindProperty]
public Contact Contact { get; set; }

public async Task<IActionResult> OnGetAsync(int id)


{
Contact = await Context.Contact.FirstOrDefaultAsync(
m => m.ContactId == id);

if (Contact == null)
{
return NotFound();
}

var isAuthorized = await AuthorizationService.AuthorizeAsync(


User, Contact,
ContactOperations.Delete);
if (!isAuthorized.Succeeded)
{
return new ChallengeResult();
}

return Page();
}

public async Task<IActionResult> OnPostAsync(int id)


{
Contact = await Context.Contact.FindAsync(id);

var contact = await Context


.Contact.AsNoTracking()
.FirstOrDefaultAsync(m => m.ContactId == id);

if (contact == null)
{
return NotFound();
}

var isAuthorized = await AuthorizationService.AuthorizeAsync(


User, contact,
ContactOperations.Delete);
if (!isAuthorized.Succeeded)
{
return new ChallengeResult();
}

Context.Contact.Remove(Contact);
await Context.SaveChangesAsync();

return RedirectToPage("./Index");
}
}
Insertar el servicio de autorización en las vistas
Actualmente, se muestra en la interfaz de usuario edita y elimina los vínculos de datos que no se puede modificar
el usuario. La interfaz de usuario es fijo aplicando el controlador de autorización a las vistas.
Insertar el servicio de autorización en el Views/_ViewImports.cshtml para que esté disponible para todas las vistas
de archivos:

@using Microsoft.AspNetCore.Identity
@using ContactManager
@using ContactManager.Data
@namespace ContactManager.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@using ContactManager.Authorization;
@using Microsoft.AspNetCore.Authorization
@using ContactManager.Models
@inject IAuthorizationService AuthorizationService

El marcado anterior agrega varias using instrucciones.


Actualización de la editar y eliminar vincula Pages/Contacts/Index.cshtml por lo que solo se procesan para los
usuarios con los permisos adecuados:

@page
@model ContactManager.Pages.Contacts.IndexModel

@{
ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
<a asp-page="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Contact[0].Name)
</th>
<th>
@Html.DisplayNameFor(model => model.Contact[0].Address)
</th>
<th>
@Html.DisplayNameFor(model => model.Contact[0].City)
</th>
<th>
@Html.DisplayNameFor(model => model.Contact[0].State)
</th>
<th>
@Html.DisplayNameFor(model => model.Contact[0].Zip)
</th>
<th>
@Html.DisplayNameFor(model => model.Contact[0].Email)
</th>
<th>
@Html.DisplayNameFor(model => model.Contact[0].Status)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Contact)
{
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Name)
</td>
<td>
@Html.DisplayFor(modelItem => item.Address)
</td>
<td>
@Html.DisplayFor(modelItem => item.City)
</td>
<td>
@Html.DisplayFor(modelItem => item.State)
</td>
<td>
@Html.DisplayFor(modelItem => item.Zip)
</td>

<td>
@Html.DisplayFor(modelItem => item.Email)
</td>

<td>
@Html.DisplayFor(modelItem => item.Status)
</td>
<td>
@if ((await AuthorizationService.AuthorizeAsync(
User, item,
ContactOperations.Update)).Succeeded)
{
<a asp-page="./Edit" asp-route-id="@item.ContactId">Edit</a>
<text> | </text>
}

<a asp-page="./Details" asp-route-id="@item.ContactId">Details</a>

@if ((await AuthorizationService.AuthorizeAsync(


User, item,
ContactOperations.Delete)).Succeeded)
{
<text> | </text>
<a asp-page="./Delete" asp-route-id="@item.ContactId">Delete</a>
}
</td>
</tr>
}
</tbody>
</table>

WARNING
Ocultar los vínculos de los usuarios que no tienen permiso para cambiar los datos no proteger la aplicación. Ocultar los
vínculos hace que la aplicación más fácil de usar mostrando vínculos solo es válido. Los usuarios pueden hack las direcciones
URL generadas para invocar Editar y eliminar operaciones en los datos que no poseen. La página de Razor o el controlador
debe exigir comprobaciones de acceso para proteger los datos.

Detalles de la actualización
Actualice la vista de detalles para que los administradores pueden aprobar o rechazar contactos:
@*Precedng markup omitted for brevity.*@

<dd>
@Html.DisplayFor(model => model.Contact.Email)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Contact.Status)
</dt>
<dd>
@Html.DisplayFor(model => model.Contact.Status)
</dd>
</dl>
</div>

@if (Model.Contact.Status != ContactStatus.Approved)


{
@if ((await AuthorizationService.AuthorizeAsync(
User, Model.Contact, ContactOperations.Approve)).Succeeded)
{
<form style="display:inline;" method="post">
<input type="hidden" name="id" value="@Model.Contact.ContactId" />
<input type="hidden" name="status" value="@ContactStatus.Approved" />
<button type="submit" class="btn btn-xs btn-success">Approve</button>
</form>
}
}

@if (Model.Contact.Status != ContactStatus.Rejected)


{
@if ((await AuthorizationService.AuthorizeAsync(
User, Model.Contact, ContactOperations.Reject)).Succeeded)
{
<form style="display:inline;" method="post">
<input type="hidden" name="id" value="@Model.Contact.ContactId" />
<input type="hidden" name="status" value="@ContactStatus.Rejected" />
<button type="submit" class="btn btn-xs btn-success">Reject</button>
</form>
}
}

<div>
@if ((await AuthorizationService.AuthorizeAsync(
User, Model.Contact,
ContactOperations.Update)).Succeeded)
{
<a asp-page="./Edit" asp-route-id="@Model.Contact.ContactId">Edit</a>
<text> | </text>
}
<a asp-page="./Index">Back to List</a>
</div>

Actualice el modelo de páginas de detalles:


public class DetailsModel : DI_BasePageModel
{
public DetailsModel(
ApplicationDbContext context,
IAuthorizationService authorizationService,
UserManager<ApplicationUser> userManager)
: base(context, authorizationService, userManager)
{
}

public Contact Contact { get; set; }

public async Task<IActionResult> OnGetAsync(int id)


{
Contact = await Context.Contact.FirstOrDefaultAsync(m => m.ContactId == id);

if (Contact == null)
{
return NotFound();
}
return Page();
}

public async Task<IActionResult> OnPostAsync(int id, ContactStatus status)


{
var contact = await Context.Contact.FirstOrDefaultAsync(
m => m.ContactId == id);

if (contact == null)
{
return NotFound();
}

var contactOperation = (status == ContactStatus.Approved)


? ContactOperations.Approve
: ContactOperations.Reject;

var isAuthorized = await AuthorizationService.AuthorizeAsync(User, contact,


contactOperation);
if (!isAuthorized.Succeeded)
{
return new ChallengeResult();
}
contact.Status = status;
Context.Contact.Update(contact);
await Context.SaveChangesAsync();

return RedirectToPage("./Index");
}
}

Probar la aplicación completada


Si está usando Visual Studio Code o pruebas en una plataforma local que no incluye un certificado de prueba
para HTTPS:
Establecer "LocalTest:skipHTTPS": true en el appsettings. Developement.JSON archivo para omitir el
requisito de HTTPS. Skip HTTPS solamente en un equipo de desarrollo.
Si la aplicación tiene contactos:
Eliminar todos los registros en la Contact tabla.
Reinicie la aplicación para inicializar la base de datos.
Registrar un usuario para examinar los contactos.
Una manera sencilla de probar la aplicación final consiste en iniciar tres distintos exploradores (o versiones de
incógnito/InPrivate). En un explorador, registrar un nuevo usuario (por ejemplo, test@example.com ). Inicie sesión
con un usuario diferente en cada explorador. Compruebe las operaciones siguientes:
Los usuarios registrados pueden ver todos los datos de contacto aprobados.
Los usuarios registrados pueden editar o eliminar sus propios datos.
Los administradores pueden aprobar o rechazar datos de contacto. El Details vista muestra aprobar y
rechazar botones.
Los administradores pueden aprobar o rechazar y editar o eliminar los datos.

USUARIO OPCIONES

test@example.com Editar puede/eliminar datos propios

manager@contoso.com Pueden aprobar o rechazar y editar o eliminar datos de


propietario

admin@contoso.com Puede editar o eliminar y aprobar o rechazar todos los datos

Crear un contacto en el explorador del administrador. Copie la dirección URL para eliminar y editar en el contacto
del administrador. Pegue estos vínculos en el explorador del usuario de prueba para comprobar que el usuario de
prueba no puede realizar estas operaciones.

Crear la aplicación de inicio


Crear una aplicación de páginas de Razor denominada "ContactManager"
Crear la aplicación con cuentas de usuario individuales.
Asígnele el nombre "ContactManager" para el espacio de nombres coincide con el espacio de nombres
utilizado en el ejemplo.

dotnet new webapp -o ContactManager -au Individual -uld

NOTE
In ASP.NET Core 2.1 or later, webapp is an alias of the razor argument. If the dotnet new webapp <OPTIONS> command
loads the dotnet new command help instead of creating a new Razor Pages app, install the .NET Core 2.1 SDK.

dotnet new razor -o ContactManager -au Individual -uld

-uld Especifica LocalDB en lugar de SQLite


Agregue las siguientes Contact modelo:
public class Contact
{
public int ContactId { get; set; }
public string Name { get; set; }
public string Address { get; set; }
public string City { get; set; }
public string State { get; set; }
public string Zip { get; set; }
[DataType(DataType.EmailAddress)]
public string Email { get; set; }
}

Scaffold el Contact modelo:

dotnet aspnet-codegenerator razorpage -m Contact -udl -dc ApplicationDbContext -outDir Pages\Contacts --


referenceScriptLibraries

Actualización de la ContactManager fijar en el Pages/_Layout.cshtml archivo:

<a asp-page="/Contacts/Index" class="navbar-brand">ContactManager</a>

Aplicar la técnica scaffolding la migración inicial y actualizar la base de datos:

dotnet ef migrations add initial


dotnet ef database update

Probar la aplicación mediante la creación, edición y eliminación de un contacto


Inicializar la base de datos
Agregar el SeedData clase a la datos carpeta. Si ha descargado el ejemplo, puede copiar la SeedData.cs del
archivo a la datos carpeta del proyecto de inicio.
Llame a SeedData.Initialize de Main :
public class Program
{
public static void Main(string[] args)
{
var host = BuildWebHost(args);

using (var scope = host.Services.CreateScope())


{
var services = scope.ServiceProvider;
var context = services.GetRequiredService<ApplicationDbContext>();
context.Database.Migrate();

try
{
SeedData.Initialize(services, "").Wait();
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred while seeding the database.");
throw ex;
}
}

host.Run();
}

public static IWebHost BuildWebHost(string[] args) =>


WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.Build();
}

Compruebe que la aplicación había propagado la base de datos. Si no hay ninguna fila en la base de datos de
contacto, no se ejecuta el método de inicialización.
Recursos adicionales
Laboratorio de autorización de ASP.NET Core. Este laboratorio se explica con más detalle en las características
de seguridad introducidas en este tutorial.
Autorización de ASP.NET Core: Simple, rol, basada en notificaciones y personalizado
Autorización personalizada basada en directivas
Convenciones de autorización de páginas de Razor
en ASP.NET Core
22/06/2018 • 5 minutes to read • Edit Online

Por Luke Latham


Una manera de controlar el acceso de la aplicación de las páginas de Razor es usar las convenciones de
autorización en el inicio. Estas convenciones permiten autorizar a los usuarios y permitir que los usuarios
anónimos pueden tener acceso a las páginas individuales o carpetas de páginas. Se aplican las convenciones
descritas en este tema automáticamente filtros de autorización para controlar el acceso.
Vea o descargue el código de ejemplo (cómo descargarlo)
Los usos de la aplicación de ejemplo autenticación con cookies sin ASP.NET Core Identity. La cuenta de usuario
para el usuario hipotética, Maria Rodríguez, está codificada en la aplicación. Use el nombre de usuario de correo
electrónico "maria.rodriguez@contoso.com" y una contraseña para iniciar sesión en el usuario. El usuario se
autentica en el AuthenticateUser método en el Pages/Account/Login.cshtml.cs archivo. En un ejemplo del mundo
real, se debería autenticar el usuario en una base de datos. Para usar ASP.NET Core Identity, siga las instrucciones
de la Introducción a la identidad en ASP.NET Core tema. Los conceptos y ejemplos que se muestran en este tema
se aplican por igual a las aplicaciones que usan ASP.NET Core Identity.

Requerir autorización para acceder a una página


Use la AuthorizePage convención a través de AddRazorPagesOptions para agregar una AuthorizeFilter a la página
en la ruta especificada:

services.AddMvc()
.AddRazorPagesOptions(options =>
{
options.Conventions.AuthorizePage("/Contact");
options.Conventions.AuthorizeFolder("/Private");
options.Conventions.AllowAnonymousToPage("/Private/PublicPage");
options.Conventions.AllowAnonymousToFolder("/Private/PublicPages");
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

La ruta de acceso especificada es la ruta de acceso del motor de vista, que es la ruta de acceso relativa de las
páginas de Razor raíz sin una extensión y que contiene solo barras diagonales.
Un AuthorizePage sobrecarga está disponible si tiene que especificar una directiva de autorización.

NOTE
Un AuthorizeFilter se puede aplicar a una clase de modelo de página con el [Authorize] atributo de filtro. Para
obtener más información, consulte atributo de filtro de autorizar.

Requerir autorización para acceder a una carpeta de páginas


Use la AuthorizeFolder convención a través de AddRazorPagesOptions para agregar una AuthorizeFilter a todas
las páginas en una carpeta en la ruta especificada:
services.AddMvc()
.AddRazorPagesOptions(options =>
{
options.Conventions.AuthorizePage("/Contact");
options.Conventions.AuthorizeFolder("/Private");
options.Conventions.AllowAnonymousToPage("/Private/PublicPage");
options.Conventions.AllowAnonymousToFolder("/Private/PublicPages");
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

La ruta de acceso especificada es la ruta de acceso del motor de vista, que es la ruta de acceso relativa de las
páginas de Razor raíz.
Un AuthorizeFolder sobrecarga está disponible si tiene que especificar una directiva de autorización.

Permitir el acceso anónimo a una página


Use la AllowAnonymousToPage convención a través de AddRazorPagesOptions para agregar una
AllowAnonymousFilter a una página en la ruta especificada:

services.AddMvc()
.AddRazorPagesOptions(options =>
{
options.Conventions.AuthorizePage("/Contact");
options.Conventions.AuthorizeFolder("/Private");
options.Conventions.AllowAnonymousToPage("/Private/PublicPage");
options.Conventions.AllowAnonymousToFolder("/Private/PublicPages");
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

La ruta de acceso especificada es la ruta de acceso del motor de vista, que es la ruta de acceso relativa de las
páginas de Razor raíz sin una extensión y que contiene solo barras diagonales.

Permitir el acceso anónimo a una carpeta de páginas


Use la AllowAnonymousToFolder convención a través de AddRazorPagesOptions para agregar una
AllowAnonymousFilter a todas las páginas en una carpeta en la ruta especificada:

services.AddMvc()
.AddRazorPagesOptions(options =>
{
options.Conventions.AuthorizePage("/Contact");
options.Conventions.AuthorizeFolder("/Private");
options.Conventions.AllowAnonymousToPage("/Private/PublicPage");
options.Conventions.AllowAnonymousToFolder("/Private/PublicPages");
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

La ruta de acceso especificada es la ruta de acceso del motor de vista, que es la ruta de acceso relativa de las
páginas de Razor raíz.

Tenga en cuenta en combinar el acceso autorizado y anónimo


Es perfectamente válido para especificar que una carpeta de páginas requieren autorización y especificar que una
página dentro de esa carpeta permite el acceso anónimo:
// This works.
.AuthorizeFolder("/Private").AllowAnonymousToPage("/Private/Public")

Sin embargo, lo contrario, no es cierto. No se puede declarar una carpeta de páginas para el acceso anónimo y
especificar una página dentro de autorización:

// This doesn't work!


.AllowAnonymousToFolder("/Public").AuthorizePage("/Public/Private")

Que requieren la autorización en la página privada no funcionará porque cuando tanto el AllowAnonymousFilter y
AuthorizeFilter filtros se aplican a la página, el AllowAnonymousFilter wins y controla el acceso.

Recursos adicionales
Proveedores personalizados de rutas y modelos de página de páginas de Razor
PageConventionCollection (clase)
Simple de autorización en ASP.NET Core
22/06/2018 • 2 minutes to read • Edit Online

Autorización en MVC se controla mediante el AuthorizeAttribute atributo y sus parámetros distintos. En su


forma más sencilla, aplicar el AuthorizeAttribute atributo a una acción o controlador se limita el acceso al
controlador o acción a cualquier usuario autenticado.
Por ejemplo, el siguiente código limita el acceso a la AccountController a cualquier usuario autenticado.

[Authorize]
public class AccountController : Controller
{
public ActionResult Login()
{
}

public ActionResult Logout()


{
}
}

Si desea aplicar la autorización a una acción en lugar de con el controlador, se aplican los AuthorizeAttribute
atribuir a la propia acción:

public class AccountController : Controller


{
public ActionResult Login()
{
}

[Authorize]
public ActionResult Logout()
{
}
}

Ahora sólo los usuarios autenticados pueden tener acceso a la Logout (función).
También puede usar el AllowAnonymous atributo para permitir el acceso a los usuarios no autenticados para
acciones individuales. Por ejemplo:

[Authorize]
public class AccountController : Controller
{
[AllowAnonymous]
public ActionResult Login()
{
}

public ActionResult Logout()


{
}
}

Esto le permitiría solo los usuarios autenticados para la AccountController , excepto para el Login acción, que
es accesible para todos los usuarios, independientemente de su estado autenticado o no autenticado / anónimo.

WARNING
[AllowAnonymous] omite todas las instrucciones de autorización. Si aplica la combinación [AllowAnonymous] y
cualquier [Authorize] siempre se omitirá el atributo, a continuación, los atributos de autorizar. Por ejemplo, si aplica
[AllowAnonymous] en el controlador de nivel de cualquier [Authorize] se pasará por alto los atributos en el mismo
controlador, o en cualquier acción en él.
Autorización basada en roles en ASP.NET Core
22/06/2018 • 4 minutes to read • Edit Online

Cuando se crea una identidad puede pertenecer a uno o más roles. Por ejemplo, Tracy puede pertenecer a los
roles de administrador y usuario aunque Scott solo puede pertenecer al rol de usuario. ¿Cómo se crean y
administran estas funciones depende del almacén de copias de seguridad del proceso de autorización. Las
funciones se exponen al programador a través de la IsInRole método en el ClaimsPrincipal clase.

Agregar comprobaciones de la función


Comprobaciones de autorización basada en roles son declarativas—el desarrollador incrusta dentro de su código,
con un controlador o una acción en un controlador, especificar las funciones que el usuario actual debe ser un
miembro de tener acceso al recurso solicitado.
Por ejemplo, el siguiente código limita el acceso a todas las acciones en el AdministrationController a los
usuarios que forman parte de la Administrator rol:

[Authorize(Roles = "Administrator")]
public class AdministrationController : Controller
{
}

Puede especificar varios roles como una lista separada por comas:

[Authorize(Roles = "HRManager,Finance")]
public class SalaryController : Controller
{
}

Este controlador sería solo sea accesible por usuarios que son miembros de la HRManager rol o la Finance rol.
Si aplica varios atributos de un usuario que obtiene acceso debe ser miembro de todos los roles especificados; el
ejemplo siguiente requiere que un usuario debe ser miembro de la PowerUser y ControlPanelUser rol.

[Authorize(Roles = "PowerUser")]
[Authorize(Roles = "ControlPanelUser")]
public class ControlPanelController : Controller
{
}

Puede limitar aún más el acceso mediante la aplicación de atributos de autorización de rol adicionales en el nivel
de acción:
[Authorize(Roles = "Administrator, PowerUser")]
public class ControlPanelController : Controller
{
public ActionResult SetTime()
{
}

[Authorize(Roles = "Administrator")]
public ActionResult ShutDown()
{
}
}

En los miembros de fragmento de código anterior de la Administrator rol o la PowerUser rol puede tener acceso
al controlador y el SetTime acción, pero solo los miembros de la Administrator rol puede tener acceso a la
ShutDown acción.

También puede bloquear un controlador pero permitir el acceso anónimo, no autenticado a acciones individuales.

[Authorize]
public class ControlPanelController : Controller
{
public ActionResult SetTime()
{
}

[AllowAnonymous]
public ActionResult Login()
{
}
}

Comprobaciones de la función basada en directivas


También se pueden expresar requisitos del rol utilizando la nueva sintaxis de directiva, donde un desarrollador
registra una directiva durante el inicio como parte de la configuración de servicio de autorización. Normalmente,
esto ocurre en ConfigureServices() en su Startup.cs archivo.

public void ConfigureServices(IServiceCollection services)


{
services.AddMvc();

services.AddAuthorization(options =>
{
options.AddPolicy("RequireAdministratorRole", policy => policy.RequireRole("Administrator"));
});
}

Las directivas se aplican mediante el Policy propiedad en el AuthorizeAttribute atributo:

[Authorize(Policy = "RequireAdministratorRole")]
public IActionResult Shutdown()
{
return View();
}

Si desea especificar varios roles permitidos en un requisito, a continuación, puede especificar como parámetros a
la RequireRole método:
options.AddPolicy("ElevatedRights", policy =>
policy.RequireRole("Administrator", "PowerUser", "BackupAdministrator"));

En este ejemplo se autoriza a los usuarios que pertenecen a la Administrator , PowerUser o BackupAdministrator
roles.
Autorización basada en notificaciones en ASP.NET
Core
22/06/2018 • 6 minutes to read • Edit Online

Cuando se crea una identidad se pueden asignar una o más notificaciones emitidos por una entidad de confianza.
Una notificación es un par nombre-valor que representa qué el asunto, puede hacerlo no qué el sujeto. Por
ejemplo, puede tener un permiso de conducción, emitido por una entidad de licencia de conducir local. De
conducir su permiso tiene tu fecha de nacimiento. En este caso, el nombre de notificación sería DateOfBirth , el
valor de notificación sería tu fecha de nacimiento, por ejemplo 8th June 1970 y el emisor sería la autoridad de
licencia de conducir. Autorización basada en notificaciones, en su forma más sencilla, comprueba el valor de una
notificación y permite el acceso a un recurso basado en ese valor. Por ejemplo, si desea que el proceso de
autorización el acceso a un club nocturno puede ser:
El responsable de seguridad de la puerta evalúe el valor de la fecha de nacimiento notificación y si confía en el
emisor (la entidad de licencia determinante) antes de conceder que acceso.
Una identidad puede contener varias notificaciones con varios valores y puede contener varias notificaciones del
mismo tipo.

Agregar controles de notificaciones


Notificación de comprobaciones de autorización basados en son declarativas, el desarrollador incrusta dentro de
su código, con un controlador o una acción en un controlador de la especificación de notificaciones que debe
poseer el usuario actual y, opcionalmente, debe contener el valor de la notificación para tener acceso a la recurso
solicitado. Notificaciones requisitos son basada en directivas, el desarrollador debe crear y registrar una directiva
de expresar los requisitos de notificaciones.
El tipo más sencillo de directiva Busca la presencia de una notificación de notificación y no comprueba el valor.
En primer lugar debe crear y registrar la directiva. Esto tiene lugar como parte de la configuración de servicio de
autorización, que normalmente toma parte en ConfigureServices() en su Startup.cs archivo.

public void ConfigureServices(IServiceCollection services)


{
services.AddMvc();

services.AddAuthorization(options =>
{
options.AddPolicy("EmployeeOnly", policy => policy.RequireClaim("EmployeeNumber"));
});
}

En este caso el EmployeeOnly directiva comprueba la presencia de un EmployeeNumber de notificación de la


identidad actual.
A continuación, aplique la directiva con la Policy propiedad en el AuthorizeAttribute atributo para especificar el
nombre de la directiva;
[Authorize(Policy = "EmployeeOnly")]
public IActionResult VacationBalance()
{
return View();
}

El AuthorizeAttribute atributo sólo se puede aplicar a un controlador todo, en esta instancia de la directiva de
coincidencia de identidades tendrán permiso para acceder a una acción en el controlador.

[Authorize(Policy = "EmployeeOnly")]
public class VacationController : Controller
{
public ActionResult VacationBalance()
{
}
}

Si tiene un controlador que está protegido por el AuthorizeAttribute de atributo, pero desea permitir el acceso
anónimo a acciones concretas que se aplica los AllowAnonymousAttribute atributo.

[Authorize(Policy = "EmployeeOnly")]
public class VacationController : Controller
{
public ActionResult VacationBalance()
{
}

[AllowAnonymous]
public ActionResult VacationPolicy()
{
}
}

La mayoría de notificaciones vienen con un valor. Puede especificar una lista de valores permitidos al crear la
directiva. En el ejemplo siguiente se realizaría correctamente solo para los empleados cuyo número de empleado
era 1, 2, 3, 4 o 5.

public void ConfigureServices(IServiceCollection services)


{
services.AddMvc();

services.AddAuthorization(options =>
{
options.AddPolicy("Founders", policy =>
policy.RequireClaim("EmployeeNumber", "1", "2", "3", "4", "5"));
});
}

Agregue una comprobación de notificación genérico


Si el valor de notificación no es un valor único o una transformación es necesaria, utilice RequireAssertion. Para
obtener más información, consulte con un elemento func para cumplir una directiva.

Evaluación múltiple de directiva


Si varias directivas se aplican a un controlador o acción, todas las directivas deben pasar antes de que se concede
el acceso. Por ejemplo:
[Authorize(Policy = "EmployeeOnly")]
public class SalaryController : Controller
{
public ActionResult Payslip()
{
}

[Authorize(Policy = "HumanResources")]
public ActionResult UpdateSalary()
{
}
}

En el ejemplo anterior cualquier identidad que cumple el EmployeeOnly directiva puede tener acceso a la Payslip
acción como esa directiva se aplica en el controlador. Sin embargo para llamar a la UpdateSalary acción debe
cumplir la identidad ambos el EmployeeOnly directiva y la HumanResources directiva.
Si desea que las directivas más complicadas, como llevar a cabo una fecha de nacimiento notificación, calcular una
edad de ella, a continuación, comprobar la edad es 21 o anterior, a continuación, tiene que escribir controladores
de directiva personalizada.
Autorización basada en directivas en ASP.NET Core
22/06/2018 • 11 minutes to read • Edit Online

Interiormente, autorización basada en roles y autorización basada en notificaciones utilizan un requisito, un


controlador de requisito y una directiva configurada previamente. Estos bloques de creación se admite la
expresión de evaluaciones de autorización en el código. El resultado es una estructura de autorización más
enriquecida, reutilizables, comprobable.
Una directiva de autorización está formada por uno o más requisitos. Se registra como parte de la configuración
de servicio de autorización, en la Startup.ConfigureServices método:

public void ConfigureServices(IServiceCollection services)


{
services.AddMvc();

services.AddAuthorization(options =>
{
options.AddPolicy("AtLeast21", policy =>
policy.Requirements.Add(new MinimumAgeRequirement(21)));
});
}

En el ejemplo anterior, se crea una directiva de "AtLeast21". Tiene un requisito único—de una antigüedad
mínima, que se proporciona como un parámetro al requisito.
Las directivas se aplican mediante el [Authorize] atributo con el nombre de la directiva. Por ejemplo:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

[Authorize(Policy = "AtLeast21")]
public class AlcoholPurchaseController : Controller
{
public IActionResult Login() => View();

public IActionResult Logout() => View();


}

Requisitos
Un requisito de autorización es una colección de parámetros de datos que puede usar una directiva para evaluar
la entidad de seguridad del usuario actual. En nuestra directiva de "AtLeast21", el requisito es un parámetro
único—la antigüedad mínima. Implementa un requisito IAuthorizationRequirement, que es una interfaz de
marcador vacío. Un requisito de antigüedad mínima con parámetros podría implementarse como sigue:
using Microsoft.AspNetCore.Authorization;

public class MinimumAgeRequirement : IAuthorizationRequirement


{
public int MinimumAge { get; private set; }

public MinimumAgeRequirement(int minimumAge)


{
MinimumAge = minimumAge;
}
}

NOTE
No tiene un requisito que tienen datos o propiedades.

Controladores de autorización
Un controlador de autorización es responsable de la evaluación de propiedades de un requisito. El controlador
de autorización evalúa los requisitos con proporcionado AuthorizationHandlerContext para determinar si se
permite el acceso.
Puede tener un requisito varios controladores. Se puede heredar un controlador
AuthorizationHandler<TRequirement >, donde TRequirement es el requisito para procesarse. Como alternativa,
puede implementar un controlador IAuthorizationHandler para administrar más de un tipo de requisito.
Usar un controlador para uno de los requisitos
El siguiente es un ejemplo de una relación uno a uno en el que un controlador de antigüedad mínima emplea un
requisito único:
using Microsoft.AspNetCore.Authorization;
using PoliciesAuthApp1.Services.Requirements;
using System;
using System.Security.Claims;
using System.Threading.Tasks;

public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>


{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
MinimumAgeRequirement requirement)
{
if (!context.User.HasClaim(c => c.Type == ClaimTypes.DateOfBirth &&
c.Issuer == "http://contoso.com"))
{
//TODO: Use the following if targeting a version of
//.NET Framework older than 4.6:
// return Task.FromResult(0);
return Task.CompletedTask;
}

var dateOfBirth = Convert.ToDateTime(


context.User.FindFirst(c => c.Type == ClaimTypes.DateOfBirth &&
c.Issuer == "http://contoso.com").Value);

int calculatedAge = DateTime.Today.Year - dateOfBirth.Year;


if (dateOfBirth > DateTime.Today.AddYears(-calculatedAge))
{
calculatedAge--;
}

if (calculatedAge >= requirement.MinimumAge)


{
context.Succeed(requirement);
}

//TODO: Use the following if targeting a version of


//.NET Framework older than 4.6:
// return Task.FromResult(0);
return Task.CompletedTask;
}
}

El código anterior determina si la entidad de seguridad del usuario actual tiene una fecha de nacimiento de
notificación de que se ha emitido por un emisor conocido y de confianza. No se puede realizar la autorización
cuando falta la notificación, en cuyo caso se devuelve una tarea completa. Cuando está presente una notificación,
se calcula la edad del usuario. Si el usuario cumple con la antigüedad mínima definida por el requisito, la
autorización se considere correcta. Cuando se realiza correctamente, la autorización context.Succeed se invoca
con el requisito satisfecho como su único parámetro.
Usar un controlador para varios requisitos
El siguiente es un ejemplo de una relación de uno a varios en el que un controlador de permiso utiliza tres
requisitos:
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using PoliciesAuthApp1.Services.Requirements;

public class PermissionHandler : IAuthorizationHandler


{
public Task HandleAsync(AuthorizationHandlerContext context)
{
var pendingRequirements = context.PendingRequirements.ToList();

foreach (var requirement in pendingRequirements)


{
if (requirement is ReadPermission)
{
if (IsOwner(context.User, context.Resource) ||
IsSponsor(context.User, context.Resource))
{
context.Succeed(requirement);
}
}
else if (requirement is EditPermission ||
requirement is DeletePermission)
{
if (IsOwner(context.User, context.Resource))
{
context.Succeed(requirement);
}
}
}

//TODO: Use the following if targeting a version of


//.NET Framework older than 4.6:
// return Task.FromResult(0);
return Task.CompletedTask;
}

private bool IsOwner(ClaimsPrincipal user, object resource)


{
// Code omitted for brevity

return true;
}

private bool IsSponsor(ClaimsPrincipal user, object resource)


{
// Code omitted for brevity

return true;
}
}

Recorre el código anterior PendingRequirements—una propiedad que contiene requisitos no marcado como
correcta. Si el usuario tiene permiso de lectura, que debe ser un propietario o un patrocinador para tener acceso
al recurso solicitado. Si el usuario tiene editar o eliminar permiso, debe un propietario para tener acceso al
recurso solicitado. Cuando se realiza correctamente, la autorización context.Succeed se invoca con el requisito
satisfecho como su único parámetro.
Registro del controlador
Los controladores se registran en la colección de servicios durante la configuración. Por ejemplo:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();

services.AddAuthorization(options =>
{
options.AddPolicy("AtLeast21", policy =>
policy.Requirements.Add(new MinimumAgeRequirement(21)));
});

services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();
}

Cada controlador se agrega a la colección de servicios mediante la invocación de


services.AddSingleton<IAuthorizationHandler, YourHandlerClass>(); .

¿Qué debe devolver un controlador?


Tenga en cuenta que la Handle método en el controlador (ejemplo) no devuelve ningún valor. ¿Cómo es un
estado de correcto o erróneo indicadas?
Un controlador indica éxito mediante una llamada a
context.Succeed(IAuthorizationRequirement requirement) , pasando el requisito de que se ha validado
correctamente.
Un controlador no tiene que controlar los errores por lo general, como otros controladores para el mismo
requisito pueden realizarse correctamente.
Para garantizar el error, incluso si otros controladores de requisito correctamente, llame a context.Fail .

Cuando se establece en false , InvokeHandlersAfterFailure propiedad (disponible en ASP.NET Core 1.1 y


versiones posterior) cortocircuita la ejecución de controladores cuando context.Fail se llama.
InvokeHandlersAfterFailure valor predeterminado es true , en cuyo caso se llama a todos los controladores.
Esto permite que los requisitos producir efectos secundarios, como el registro, que siempre tienen lugar incluso
si context.Fail se ha llamado en otro controlador.

¿Por qué desearía varios controladores para un requisito?


En casos donde probablemente prefiera evaluación en un o base, implementar varios controladores para un
requisito único. Por ejemplo, Microsoft tiene puertas que solo se abren con tarjetas de clave. Si deja la tarjeta de
claves en casa, la recepcionista imprime una etiqueta temporal y abre la puerta para usted. En este escenario,
tendría un requisito único, BuildingEntry, pero varios controladores, cada uno de ellos examinando un requisito
único.
BuildingEntryRequirement.cs

using Microsoft.AspNetCore.Authorization;

public class BuildingEntryRequirement : IAuthorizationRequirement


{
}

BadgeEntryHandler.cs
using Microsoft.AspNetCore.Authorization;
using PoliciesAuthApp1.Services.Requirements;
using System.Security.Claims;
using System.Threading.Tasks;

public class BadgeEntryHandler : AuthorizationHandler<BuildingEntryRequirement>


{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
BuildingEntryRequirement requirement)
{
if (context.User.HasClaim(c => c.Type == ClaimTypes.BadgeId &&
c.Issuer == "http://microsoftsecurity"))
{
context.Succeed(requirement);
}

//TODO: Use the following if targeting a version of


//.NET Framework older than 4.6:
// return Task.FromResult(0);
return Task.CompletedTask;
}
}

TemporaryStickerHandler.cs

using Microsoft.AspNetCore.Authorization;
using PoliciesAuthApp1.Services.Requirements;
using System.Security.Claims;
using System.Threading.Tasks;

public class TemporaryStickerHandler : AuthorizationHandler<BuildingEntryRequirement>


{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
BuildingEntryRequirement requirement)
{
if (context.User.HasClaim(c => c.Type == ClaimTypes.TemporaryBadgeId &&
c.Issuer == "https://microsoftsecurity"))
{
// We'd also check the expiration date on the sticker.
context.Succeed(requirement);
}

//TODO: Use the following if targeting a version of


//.NET Framework older than 4.6:
// return Task.FromResult(0);
return Task.CompletedTask;
}
}

Asegúrese de que ambos controladores estén registrado. Si cualquier controlador se ejecuta correctamente
cuando una directiva se evalúa como el BuildingEntryRequirement , la evaluación de directivas se realiza
correctamente.

Usar un elemento func para cumplir una directiva


Puede haber situaciones en las que cumplir una directiva es simple expresar en el código. Es posible
proporcionar un Func<AuthorizationHandlerContext, bool> al configurar la directiva con el RequireAssertion el
generador de directiva.
Por ejemplo, el anterior BadgeEntryHandler podría volver a escribir como se indica a continuación:
services.AddAuthorization(options =>
{
options.AddPolicy("BadgeEntry", policy =>
policy.RequireAssertion(context =>
context.User.HasClaim(c =>
(c.Type == ClaimTypes.BadgeId ||
c.Type == ClaimTypes.TemporaryBadgeId) &&
c.Issuer == "https://microsoftsecurity")));
});

Acceso al contexto de solicitud MVC en los controladores


El HandleRequirementAsyncmétodo se implementa en un controlador de autorización tiene dos parámetros: una
AuthorizationHandlerContext y TRequirement está controlando. Marcos de trabajo como MVC o Jabbr pueden
agregar cualquier objeto a la Resource propiedad en el AuthorizationHandlerContext para pasar información
adicional.
Por ejemplo, MVC pasa una instancia de AuthorizationFilterContext en el Resource propiedad. Esta propiedad
proporciona acceso a HttpContext , RouteData y todo el contenido más proporcionó MVC y las páginas de Razor.
El uso de la Resource propiedad es específicos de la plataforma. De manera indicada en el Resource propiedad
limita las directivas de autorización para marcos de trabajo determinados. Debe convertir el Resource propiedad
mediante la as (palabra clave) y, a continuación, confirme la conversión se realizará correctamente para
asegurarse de que el código de no bloqueo con un InvalidCastException cuando se ejecuta en otros marcos de
trabajo:

// Requires the following import:


// using Microsoft.AspNetCore.Mvc.Filters;
if (context.Resource is AuthorizationFilterContext mvcContext)
{
// Examine MVC-specific things like routing data.
}
Inserción de dependencias en los controladores de
requisito en ASP.NET Core
22/06/2018 • 2 minutes to read • Edit Online

Controladores de autorización deben estar registrados en la colección de servicio durante la configuración


(mediante inyección de dependencia).
Suponga que tiene un repositorio de reglas que desea evaluar dentro de un controlador de autorización y ese
repositorio se ha registrado en la colección de servicio. La autorización se resuelva e insertar en el constructor.
Por ejemplo, si desea utilizar ASP. NET del registro de infraestructura que desea insertar ILoggerFactory en el
controlador. Un controlador de este tipo podría ser similar:

public class LoggingAuthorizationHandler : AuthorizationHandler<MyRequirement>


{
ILogger _logger;

public LoggingAuthorizationHandler(ILoggerFactory loggerFactory)


{
_logger = loggerFactory.CreateLogger(this.GetType().FullName);
}

protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, MyRequirement


requirement)
{
_logger.LogInformation("Inside my handler");
// Check if the requirement is fulfilled.
return Task.CompletedTask;
}
}

Debería registrar el controlador con services.AddSingleton() :

services.AddSingleton<IAuthorizationHandler, LoggingAuthorizationHandler>();

Una instancia de la voluntad de controlador se crea cuando se inicia la aplicación y se DI inyectar registrado
ILoggerFactory en su constructor.

NOTE
Los controladores que usan Entity Framework no deben registrarse como singleton.
Autorización basada en recursos en ASP.NET Core
22/06/2018 • 8 minutes to read • Edit Online

Estrategia de autorización depende de los recursos que se obtiene acceso. Considere la posibilidad de un
documento que tiene una propiedad de autor. Solo el autor tiene permiso para actualizar el documento. Por lo
tanto, el documento se debe recuperar desde el almacén de datos antes de que puede producirse la evaluación de
autorización.
Evaluación de atributo se produce antes del enlace de datos y antes de la ejecución del controlador de la página o
acción que carga el documento. Por estos motivos, autorización declarativa con un [Authorize] atributo no son
suficientes. En su lugar, puede invocar un método de autorización personalizado—un estilo que se conoce como
autorización imperativa.
Use la aplicaciones de ejemplo (cómo descargar) para explorar las características descritas en este tema.
Crear una aplicación de ASP.NET Core con datos de usuario protegidos por autorización contiene una aplicación
de ejemplo que utiliza la autorización basada en recursos.

Utilizar la autorización imperativa


Autorización se implementa como un IAuthorizationService de servicio y está registrado en la colección de
servicio en la Startup clase. El servicio debe ponerse a disposición a través de inyección de dependencia a
controladores de página o acciones.

public class DocumentController : Controller


{
private readonly IAuthorizationService _authorizationService;
private readonly IDocumentRepository _documentRepository;

public DocumentController(IAuthorizationService authorizationService,


IDocumentRepository documentRepository)
{
_authorizationService = authorizationService;
_documentRepository = documentRepository;
}

IAuthorizationService tiene dos AuthorizeAsync sobrecargas del método: acepta un recurso y, a continuación, el
nombre de directiva y el otro aceptando el recurso y una lista de requisitos para evaluar.
ASP.NET Core 2.x
ASP.NET Core 1.x

Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user,


object resource,
IEnumerable<IAuthorizationRequirement> requirements);
Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user,
object resource,
string policyName);

En el ejemplo siguiente, se carga el recurso que se va a proteger en un personalizado Document objeto. Un


AuthorizeAsync sobrecarga se invoca para determinar si se permite al usuario actual para modificar el documento
proporcionado. Una directiva de autorización personalizada "EditPolicy" es tenerse en cuenta en la decisión. Vea
autorización basada en directivas de personalizado para obtener más información acerca de cómo crear directivas
de autorización.

NOTE
El código siguiente ejemplos se supone la autenticación se ha ejecutado y establezca el User propiedad.

ASP.NET Core 2.x


ASP.NET Core 1.x

public async Task<IActionResult> OnGetAsync(Guid documentId)


{
Document = _documentRepository.Find(documentId);

if (Document == null)
{
return new NotFoundResult();
}

var authorizationResult = await _authorizationService


.AuthorizeAsync(User, Document, "EditPolicy");

if (authorizationResult.Succeeded)
{
return Page();
}
else if (User.Identity.IsAuthenticated)
{
return new ForbidResult();
}
else
{
return new ChallengeResult();
}
}

Escribir un controlador basada en recursos


Escribir un controlador para la autorización basada en recursos no es muy diferente de escribir un controlador de
requisitos sin formato. Cree una clase de requisito personalizado e implementar una clase de controlador de
requisito. La clase de controlador especifica el requisito y el tipo de recurso. Por ejemplo, un controlador utilizando
un SameAuthorRequirement requisito y un Document recurso tiene el siguiente aspecto:
ASP.NET Core 2.x
ASP.NET Core 1.x
public class DocumentAuthorizationHandler :
AuthorizationHandler<SameAuthorRequirement, Document>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
SameAuthorRequirement requirement,
Document resource)
{
if (context.User.Identity?.Name == resource.Author)
{
context.Succeed(requirement);
}

return Task.CompletedTask;
}
}

public class SameAuthorRequirement : IAuthorizationRequirement { }

Registrar el requisito y el controlador en el Startup.ConfigureServices método:

services.AddMvc();

services.AddAuthorization(options =>
{
options.AddPolicy("EditPolicy", policy =>
policy.Requirements.Add(new SameAuthorRequirement()));
});

services.AddSingleton<IAuthorizationHandler, DocumentAuthorizationHandler>();
services.AddSingleton<IAuthorizationHandler, DocumentAuthorizationCrudHandler>();
services.AddScoped<IDocumentRepository, DocumentRepository>();

Requisitos operativos
Si está realizando decisiones basadas en los resultados de operaciones CRUD (creación, lectura, actualización,
eliminación), use la OperationAuthorizationRequirement clase auxiliar. Esta clase permite escribir un único
controlador en lugar de una clase individual para cada tipo de operación. Para usarla, proporcionar algunos
nombres de operación:

public static class Operations


{
public static OperationAuthorizationRequirement Create =
new OperationAuthorizationRequirement { Name = nameof(Create) };
public static OperationAuthorizationRequirement Read =
new OperationAuthorizationRequirement { Name = nameof(Read) };
public static OperationAuthorizationRequirement Update =
new OperationAuthorizationRequirement { Name = nameof(Update) };
public static OperationAuthorizationRequirement Delete =
new OperationAuthorizationRequirement { Name = nameof(Delete) };
}

El controlador se implementa como sigue, mediante una OperationAuthorizationRequirement requisito y un


Document recursos:

ASP.NET Core 2.x


ASP.NET Core 1.x
public class DocumentAuthorizationCrudHandler :
AuthorizationHandler<OperationAuthorizationRequirement, Document>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
OperationAuthorizationRequirement requirement,
Document resource)
{
if (context.User.Identity?.Name == resource.Author &&
requirement.Name == Operations.Read.Name)
{
context.Succeed(requirement);
}

return Task.CompletedTask;
}
}

El controlador anterior valida la operación con el recurso, la identidad del usuario y el requisito Name propiedad.
Para llamar a un controlador de recursos operativa, especifique la operación al invocar AuthorizeAsync en el
controlador de la página o la acción. En el ejemplo siguiente se determina si se permite al usuario autenticado para
ver el documento proporcionado.

NOTE
El código siguiente ejemplos se supone la autenticación se ha ejecutado y establezca el User propiedad.

ASP.NET Core 2.x


ASP.NET Core 1.x

public async Task<IActionResult> OnGetAsync(Guid documentId)


{
Document = _documentRepository.Find(documentId);

if (Document == null)
{
return new NotFoundResult();
}

var authorizationResult = await _authorizationService


.AuthorizeAsync(User, Document, Operations.Read);

if (authorizationResult.Succeeded)
{
return Page();
}
else if (User.Identity.IsAuthenticated)
{
return new ForbidResult();
}
else
{
return new ChallengeResult();
}
}

Si la autorización se realiza correctamente, se devuelve la página para ver el documento. Si se produce un error de
autorización, pero el usuario se autentica, devolver ForbidResult informa a cualquier middleware de autenticación
error de autorización. Un ChallengeResult se devuelve cuando se debe realizar la autenticación. Para los clientes
de explorador interactivo, puede ser adecuado redirigir al usuario a una página de inicio de sesión.
Autorización basada en la vista de MVC de ASP.NET
Core
22/06/2018 • 2 minutes to read • Edit Online

A menudo, un programador desea mostrar, ocultar o modifique una interfaz de usuario en función de la identidad
del usuario actual. Puede acceder al servicio de autorización en las vistas MVC a través de inyección de
dependencia. Para insertar el servicio de autorización en una vista Razor, use la @inject directiva:

@using Microsoft.AspNetCore.Authorization
@inject IAuthorizationService AuthorizationService

Si desea que el servicio de autorización en cada vista, coloque el @inject la directiva en el _ViewImports.cshtml
archivos de la vistas directory. Para más información, vea Dependency injection into views (Inserción de
dependencias en vistas).
Usar el servicio de autorización insertado para invocar AuthorizeAsync exactamente del mismo modo que se
protegerían durante autorización basada en recursos:
ASP.NET Core 2.x
ASP.NET Core 1.x

@if ((await AuthorizationService.AuthorizeAsync(User, "PolicyName")).Succeeded)


{
<p>This paragraph is displayed because you fulfilled PolicyName.</p>
}

En algunos casos, el recurso será el modelo de vista. Invocar AuthorizeAsync exactamente del mismo modo que se
protegerían durante autorización basada en recursos:
ASP.NET Core 2.x
ASP.NET Core 1.x

@if ((await AuthorizationService.AuthorizeAsync(User, Model, Operations.Edit)).Succeeded)


{
<p><a class="btn btn-default" role="button"
href="@Url.Action("Edit", "Document", new { id = Model.Id })">Edit</a></p>
}

En el código anterior, el modelo se pasa como un recurso que se debe realizar la evaluación de directivas en
consideración.

WARNING
No confíe en alternancia visibilidad de los elementos de interfaz de usuario de la aplicación como la comprobación de
autorización única. Ocultar un elemento de interfaz de usuario puede impedir totalmente el acceso a su acción de
controlador asociado. Por ejemplo, considere el botón en el fragmento de código anterior. Un usuario puede invocar la
Edit dirección URL del método de acción si conozca el recurso relativo es /Document/Edit/1. Por este motivo, la Edit
método de acción debe realizar su propia comprobación de autorización.
Autorizar con un esquema específico de ASP.NET
Core
22/06/2018 • 4 minutes to read • Edit Online

En algunos escenarios, como aplicaciones de una única página (SPAs), es habitual usar varios métodos de
autenticación. Por ejemplo, la aplicación puede usar la autenticación basada en cookies para iniciar sesión y
autenticación de portador JWT para las solicitudes de JavaScript. En algunos casos, la aplicación puede tener
varias instancias de un controlador de autenticación. Por ejemplo, dos controladores de la cookie donde uno
contiene una identidad básica y el otro se crea cuando se ha desencadenado una autenticación multifactor (MFA).
MFA se puede desencadenar porque el usuario solicitó una operación que requiere seguridad adicional.
ASP.NET Core 2.x
ASP.NET Core 1.x
Un esquema de autenticación se denomina cuando se configura el servicio de autenticación durante la
autenticación. Por ejemplo:

public void ConfigureServices(IServiceCollection services)


{
// Code omitted for brevity

services.AddAuthentication()
.AddCookie(options => {
options.LoginPath = "/Account/Unauthorized/";
options.AccessDeniedPath = "/Account/Forbidden/";
})
.AddJwtBearer(options => {
options.Audience = "http://localhost:5001/";
options.Authority = "http://localhost:5000/";
});

En el código anterior, se han agregado dos controladores de autenticación: uno para las cookies y otro para
portador.

NOTE
Especificar el esquema predeterminado de resultados en el HttpContext.User propiedad que se establece para esa
identidad. Si no se desea este comportamiento, puede deshabilitarlo mediante la invocación de la forma sin parámetros de
AddAuthentication .

Seleccionar el esquema con el atributo Authorize


En el momento de la autorización, la aplicación indica el controlador que se usará. Seleccione el controlador con
el que se autorizará la aplicación pasando una lista delimitada por comas de esquemas de autenticación
[Authorize] . El [Authorize] atributo especifica el esquema de autenticación o los esquemas para usar
independientemente de si se configura un valor predeterminado. Por ejemplo:
ASP.NET Core 2.x
ASP.NET Core 1.x
[Authorize(AuthenticationSchemes = AuthSchemes)]
public class MixedController : Controller
// Requires the following imports:
// using Microsoft.AspNetCore.Authentication.Cookies;
// using Microsoft.AspNetCore.Authentication.JwtBearer;
private const string AuthSchemes =
CookieAuthenticationDefaults.AuthenticationScheme + "," +
JwtBearerDefaults.AuthenticationScheme;

En el ejemplo anterior, los controladores de la cookie y el portador ejecutan y tienen una oportunidad para crear
y agregar una identidad para el usuario actual. Mediante la especificación de un único esquema, se ejecuta el
controlador correspondiente.
ASP.NET Core 2.x
ASP.NET Core 1.x

[Authorize(AuthenticationSchemes =
JwtBearerDefaults.AuthenticationScheme)]
public class MixedController : Controller

En el código anterior, se ejecuta sólo el controlador con el esquema "Portador". Se omite cualquier identidad
basada en cookies.

Seleccionar el esquema con directivas


Si desea especificar los esquemas deseados en directiva, puede establecer el AuthenticationSchemes colección
cuando se agrega la directiva:

services.AddAuthorization(options =>
{
options.AddPolicy("Over18", policy =>
{
policy.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme);
policy.RequireAuthenticatedUser();
policy.Requirements.Add(new MinimumAgeRequirement());
});
});

En el ejemplo anterior, la directiva de "Over18" solo se ejecuta con la identidad creada por el controlador
"Portador". Usar la directiva estableciendo la [Authorize] del atributo Policy propiedad:

[Authorize(Policy = "Over18")]
public class RegistrationController : Controller
Protección de datos en ASP.NET Core
21/06/2018 • 2 minutes to read • Edit Online

Introducción a la protección de datos


Introducción a las API de protección de datos
API de consumidor
Información general sobre las API de consumidor
Cadenas de propósito
Jerarquía de propósito y configuración multiempresa
Aplicar un algoritmo hash a las contraseñas
Limitación de la duración de cargas protegidas
Desprotección de cargas cuyas claves se han revocado
Configuración
Configurar la protección de datos en ASP.NET Core
Configuración predeterminada
Directiva de todo el equipo
Escenarios no compatibles con DI
API de extensibilidad
Extensibilidad de criptografía de núcleo
Extensibilidad de administración de claves
Otras API
Implementación
Detalles de cifrado autenticado
Derivación de subclave y cifrado autenticado
Encabezados de contexto
Administración de claves
Proveedores de almacenamiento de claves
Cifrado de claves en reposo
Inmutabilidad de claves y configuración
Formato de almacenamiento de claves
Proveedores de protección de datos efímeros
Compatibilidad
Reemplazar de ASP.NET en ASP.NET Core
Protección de datos de ASP.NET Core
22/06/2018 • 11 minutes to read • Edit Online

Las aplicaciones Web a menudo se necesitan almacenar datos confidenciales de seguridad. Windows proporciona
DPAPI para aplicaciones de escritorio, pero esto no es apropiada para las aplicaciones web. La pila de protección
de datos de ASP.NET Core proporcionan una API cifrada sencilla y fácil de usar un programador puede utilizar
para proteger los datos, incluida la rotación y la administración de claves.
La pila de protección de datos de ASP.NET Core está diseñada para que actúe como la sustituirá a largo plazo
para la <machineKey> elemento en ASP.NET 1.x - 4.x. Se diseñó para solucionar muchos de los defectos de la pila
cifrado anterior al proporcionar una solución para la mayoría de los casos de uso de aplicaciones modernas están
probables que encuentre.

Declaración del problema


En pocas palabras la declaración del problema general en una sola frase: se necesita para conservar información
de confianza para su recuperación posterior, pero no confía en el mecanismo de persistencia. En términos de web,
esto puede escribirse como "Necesito al estado de confianza de ida y vuelta a través de un cliente no es de
confianza".
El ejemplo canónico de esto es una cookie de autenticación o portador símbolo (token). El servidor genera un
"Estoy Groot y tiene permisos de xyz" símbolo (token) y los entrega al cliente. En una fecha futura, el cliente
presentará ese token en el servidor, pero el servidor necesita algún tipo de garantía de que el cliente no ha
falsificado el token. Por lo tanto, el primer requisito: autenticidad (conocido como) integridad, alteración de
corrección).
Puesto que el estado persistente es de confianza para el servidor, prevemos que este estado podría contener
información específica para el entorno operativo. Podría tratarse con el formato de una ruta de acceso de archivo,
un permiso, un identificador u otra referencia indirecta o alguna otra parte de los datos específicos del servidor.
Esta información normalmente no debe divulgarse a un cliente no es de confianza. Por lo tanto, el segundo
requisito: confidencialidad.
Por último, puesto que están dividida en componentes de aplicaciones modernas, lo que hemos visto es que los
componentes individuales desea aprovechar las ventajas de este sistema sin tener en cuenta otros componentes
en el sistema. Por ejemplo, si un componente de token de portador está usando esta pila, debe funcionar sin la
interferencia de un mecanismo de anti-CSRF que también puedan estar usando la misma pila. Por lo tanto, el
requisito final: aislamiento.
Podemos proporcionar más restricciones con el fin de restringir el ámbito de los requisitos. Se supone que todos
los servicios que funcionan en el sistema de cifrado se confía por igual y que no necesitan los datos que se genera
o consume fuera de los servicios en nuestro control directo. Además, es necesario que las operaciones son lo más
rápidas posible, ya que cada solicitud al servicio web puede pasar por el sistema de cifrado una o varias veces.
Esto hace que la criptografía simétrica ideal para nuestro escenario y se podemos descuentos criptografía
asimétrica hasta como uno que sea necesaria.

Filosofía de diseño
Hemos iniciado mediante la identificación de problemas con la pila del existente. Una vez que tuvimos, hemos
evaluar el panorama de las soluciones existentes y concluye que no hay ninguna solución existente bastante
tuvieron las capacidades que se busca. A continuación, diseñamos una solución basada en varias orientaciones.
El sistema debe ofrecer la sencillez de configuración. Lo ideal es que el sistema sería sin configuración y los
desarrolladores podrían llegar a cero que se ejecuta. En situaciones donde los desarrolladores deben
configurar un aspecto concreto (por ejemplo, el repositorio de claves), se debe prestar atención especial a
hacer que esas configuraciones específicas simple.
Ofrecen una API sencilla de consumo. Las API deben ser fáciles de usar correctamente y difíciles de usar
de forma incorrecta.
Los desarrolladores no deben aprender los principios de administración de claves. El sistema debe
controlar la selección de algoritmo y la vigencia de la clave en nombre del desarrollador. Lo ideal es que el
programador ni siquiera debe tener acceso a material de la clave sin formato.
Las claves deben estar protegidas en reposo cuando sea posible. El sistema debe determinar un
mecanismo de protección predeterminado adecuado y lo aplicará automáticamente.
Con estos principios en cuenta que desarrollamos una sencilla fácil de usar pila de protección de datos.
La API de protección de datos de ASP.NET Core no están pensados principalmente para la persistencia indefinido
de cargas confidenciales. Otras tecnologías, como Windows CNG DPAPI y Azure Rights Management son más
adecuados para el escenario de almacenamiento indefinido, y tienen capacidades de administración de claves
seguro según corresponda. Es decir, no hay nada prohibir a un desarrollador usa las API de protección de datos
de ASP.NET Core para la protección a largo plazo de información confidencial.

Audiencia
El sistema de protección de datos se divide en cinco paquetes principales. Destino de distintos aspectos de estas
API tres audiencias principales;
1. El información general de las API de consumidor a los desarrolladores de aplicaciones y un marco de
destino.
"No desea obtener información acerca de cómo funciona la pila o acerca de cómo esté configurada.
Simplemente desea realizar alguna operación como simple forma como sea posible con una probabilidad
alta de usar las API correctamente."
2. El API de configuración dirigidas a los desarrolladores de aplicaciones y los administradores del sistema.
"Necesito indicar que el sistema de protección de datos que mi entorno requiere configuración o las rutas
de acceso no predeterminada".
3. Desarrolladores de destino de las API de extensibilidad responsable de implementar la directiva
personalizada. Uso de estas API se usaría limitarse a situaciones excepcionales y experimentado, los
desarrolladores de cuenta de seguridad.
"Necesito reemplazar un componente completo dentro del sistema porque tienen requisitos de
comportamiento verdaderamente únicos. Estoy dispuesto a aprender uncommonly utiliza partes de la
superficie de API para compilar un complemento que cumple los requisitos de mi."

Diseño del paquete


La pila de protección de datos consta de cinco paquetes.
Microsoft.AspNetCore.DataProtection.Abstractions contiene las interfaces básicas de
IDataProtectionProvider y IDataProtector. También contiene métodos de extensión útil que pueden ayudar
a trabajar con estos tipos (p. ej., las sobrecargas de IDataProtector.Protect). Vea la sección de interfaces de
consumidor para obtener más información. Si otra persona es responsable de crear una instancia del
sistema de protección de datos y simplemente están utilizando las API, deseará referencia
Microsoft.AspNetCore.DataProtection.Abstractions.
Microsoft.AspNetCore.DataProtection contiene la implementación básica del sistema de protección de
datos, incluida la administración de claves, configuración, extensibilidad y las operaciones criptográficas de
núcleo. Si eres responsable de crear una instancia del sistema de protección de datos (p. ej., éste se agrega
a un IServiceCollection) o modificar o extender su comportamiento, deseará referencia
Microsoft.AspNetCore.DataProtection.
Microsoft.AspNetCore.DataProtection.Extensions contiene algunas API adicionales que los desarrolladores
que resulte útiles, pero no en el paquete de núcleo que pertenecen. Por ejemplo, este paquete contiene una
API sencilla "crear una instancia del sistema que señala a un directorio de almacenamiento de claves
específico con ninguna instalación de inyección de dependencia" (más información). También contiene
métodos de extensión para limitar la duración de cargas protegidos (más información).
Microsoft.AspNetCore.DataProtection.SystemWeb puede instalarse en una aplicación existente de 4.x
ASP.NET para redirigir su <machineKey> operaciones usar en su lugar la nueva pila de protección de
datos. Vea compatibilidad para obtener más información.
Microsoft.AspNetCore.Cryptography.KeyDerivation proporciona una implementación de la contraseña
PBKDF2 hash rutina y puede usarse en sistemas que necesitan para administrar las contraseñas de usuario
de forma segura. Vea Hash a contraseñas para obtener más información.
Empezar a trabajar con las API de protección de
datos en ASP.NET Core
22/06/2018 • 3 minutes to read • Edit Online

Como sus datos más sencillas, protección consta de los siguientes pasos:
1. Crear un datos protector desde un proveedor de protección de datos.
2. Llame a la Protect método con los datos que desea proteger.
3. Llame a la Unprotect método con los datos que desea convertir en texto sin formato.
La mayoría de los marcos y modelos de aplicación, por ejemplo, ASP.NET o SignalR, ya que configurar el sistema
de protección de datos y agregarlo a un contenedor de servicios que se puede acceder a través de la inserción de
dependencias. El siguiente ejemplo muestra cómo configurar un contenedor de servicios para la inyección de
dependencia y registrar la pila de protección de datos, recibe el proveedor de protección de datos a través de DI,
crear un protector y datos de protección y desprotección
using System;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.DependencyInjection;

public class Program


{
public static void Main(string[] args)
{
// add data protection services
var serviceCollection = new ServiceCollection();
serviceCollection.AddDataProtection();
var services = serviceCollection.BuildServiceProvider();

// create an instance of MyClass using the service provider


var instance = ActivatorUtilities.CreateInstance<MyClass>(services);
instance.RunSample();
}

public class MyClass


{
IDataProtector _protector;

// the 'provider' parameter is provided by DI


public MyClass(IDataProtectionProvider provider)
{
_protector = provider.CreateProtector("Contoso.MyClass.v1");
}

public void RunSample()


{
Console.Write("Enter input: ");
string input = Console.ReadLine();

// protect the payload


string protectedPayload = _protector.Protect(input);
Console.WriteLine($"Protect returned: {protectedPayload}");

// unprotect the payload


string unprotectedPayload = _protector.Unprotect(protectedPayload);
Console.WriteLine($"Unprotect returned: {unprotectedPayload}");
}
}
}

/*
* SAMPLE OUTPUT
*
* Enter input: Hello world!
* Protect returned: CfDJ8ICcgQwZZhlAlTZT...OdfH66i1PnGmpCR5e441xQ
* Unprotect returned: Hello world!
*/

Cuando se crea un protector debe proporcionar uno o varios propósito cadenas. Una cadena de propósito
proporciona aislamiento entre los consumidores. Por ejemplo, un protector creado con una cadena de fin de
"verde" no podrá desproteger los datos proporcionados por un protector con un propósito de "púrpura".
TIP
Instancias de IDataProtectionProvider y IDataProtector son seguras para subprocesos para varios de los llamadores.
Se ha diseñado que una vez que un componente obtiene una referencia a un IDataProtector mediante una llamada a
CreateProtector , utilizará dicha referencia para varias llamadas a Protect y Unprotect .

Una llamada a Unprotect iniciará CryptographicException si no se puede comprobar o descifrar la carga protegida.
Algunos componentes que desee omitir los errores durante la desproteger operaciones; un componente que lee las cookies
de autenticación puede controlar este error y tratar la solicitud como si no tuviera en absoluto ninguna cookie en lugar de
producirá un error en la solicitud directamente. Los componentes que desea este comportamiento deben detectar
específicamente CryptographicException en lugar de admitir todas las excepciones.
API de consumidor para ASP.NET Core
21/06/2018 • 2 minutes to read • Edit Online

Información general sobre las API de consumidor


Cadenas de propósito
Jerarquía de propósito y configuración multiempresa
Aplicar un algoritmo hash a las contraseñas
Limitación de la duración de cargas protegidas
Desprotección de cargas cuyas claves se han revocado
Información general de las API de consumidor para
ASP.NET Core
22/06/2018 • 7 minutes to read • Edit Online

El IDataProtectionProvider y IDataProtector interfaces son las interfaces básicas a través del cual los
consumidores utilizan el sistema de protección de datos. Se encuentran en el
Microsoft.AspNetCore.DataProtection.Abstractions paquete.

IDataProtectionProvider
La interfaz del proveedor representa la raíz del sistema de protección de datos. Directamente no puede usarse
para proteger o desproteger los datos. En su lugar, el consumidor debe obtener una referencia a un
IDataProtector mediante una llamada a IDataProtectionProvider.CreateProtector(purpose) , donde el propósito
es una cadena que describe el caso de uso previsto del consumidor. Vea propósito cadenas para mucho más
información sobre la intención de este parámetro y cómo elegir un valor adecuado.

IDataProtector
La interfaz de protector devuelto por una llamada a CreateProtector y de esta interfaz que los consumidores
pueden usar para realizar proteger y desproteger las operaciones.
Para proteger un elemento de datos, pasar los datos a la Protect método. La interfaz básica define un método
que byte convierte [] -> byte [], pero no hay también una sobrecarga (proporcionada como un método de
extensión) que convierte la cadena -> cadena. La seguridad proporcionada por los dos métodos es idéntica; el
desarrollador debe elegir cualquier sobrecarga es más conveniente para sus casos de uso. Con independencia de
la sobrecarga elegida, el valor devuelto por el proteja método ahora está protegido (descifra y compatible con
tecnologías de manipulaciones) y la aplicación puede enviar a un cliente no es de confianza.
Para desproteger un elemento de datos protegidos anteriormente, pasar los datos protegidos en el Unprotect
método. (Hay byte []-basada en cadena y basados en sobrecargas para mayor comodidad de desarrollador.) Si la
carga protegida fue generada por una llamada anterior a Protect en esta misma IDataProtector , el Unprotect
método devolverá la carga sin protección original. Si la carga protegida se ha alterado o ha sido creada por otro
IDataProtector , el Unprotect método producirá CryptographicException.

El concepto de misma frente a diferentes IDataProtector ties hacia el concepto de propósito. Si dos
IDataProtector instancias se generaron desde la misma raíz IDataProtectionProvider , pero a través de las
cadenas de propósito diferente en la llamada a IDataProtectionProvider.CreateProtector , considera que está
diferentes protectores, y uno no podrá desproteger cargas generados por el otro.

Consumir estas interfaces


Para un componente compatible con DI, el uso previsto es que el componente que pase un
IDataProtectionProvider parámetro en su constructor y compruebe que el sistema DI automáticamente
proporciona este servicio cuando se crea una instancia del componente.
NOTE
Algunas aplicaciones (por ejemplo, las aplicaciones de consola o aplicaciones de ASP.NET 4.x) podrían no ser DI compatible
con por lo que no se puede usar el mecanismo descrito aquí. Para consultar estos escenarios el no escenarios DI
documento para obtener más información sobre cómo obtener una instancia de un IDataProtection proveedor sin
pasar por DI.

El siguiente ejemplo muestra tres conceptos:


1. Agregue el sistema de protección de datos al contenedor de servicios,
2. Usar DI para recibir una instancia de un IDataProtectionProvider ,y
3. Crear un IDataProtector desde un IDataProtectionProvider y usarla para proteger y desproteger los
datos.
using System;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.DependencyInjection;

public class Program


{
public static void Main(string[] args)
{
// add data protection services
var serviceCollection = new ServiceCollection();
serviceCollection.AddDataProtection();
var services = serviceCollection.BuildServiceProvider();

// create an instance of MyClass using the service provider


var instance = ActivatorUtilities.CreateInstance<MyClass>(services);
instance.RunSample();
}

public class MyClass


{
IDataProtector _protector;

// the 'provider' parameter is provided by DI


public MyClass(IDataProtectionProvider provider)
{
_protector = provider.CreateProtector("Contoso.MyClass.v1");
}

public void RunSample()


{
Console.Write("Enter input: ");
string input = Console.ReadLine();

// protect the payload


string protectedPayload = _protector.Protect(input);
Console.WriteLine($"Protect returned: {protectedPayload}");

// unprotect the payload


string unprotectedPayload = _protector.Unprotect(protectedPayload);
Console.WriteLine($"Unprotect returned: {unprotectedPayload}");
}
}
}

/*
* SAMPLE OUTPUT
*
* Enter input: Hello world!
* Protect returned: CfDJ8ICcgQwZZhlAlTZT...OdfH66i1PnGmpCR5e441xQ
* Unprotect returned: Hello world!
*/

El paquete Microsoft.AspNetCore.DataProtection.Abstractions contiene un método de extensión


IServiceProvider.GetDataProtector como una comodidad para desarrolladores. Encapsula como una única
operación ambos recuperar un IDataProtectionProvider desde el proveedor de servicios y llamar al método
IDataProtectionProvider.CreateProtector . El ejemplo siguiente muestra su uso.
using System;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.DependencyInjection;

public class Program


{
public static void Main(string[] args)
{
// add data protection services
var serviceCollection = new ServiceCollection();
serviceCollection.AddDataProtection();
var services = serviceCollection.BuildServiceProvider();

// get an IDataProtector from the IServiceProvider


var protector = services.GetDataProtector("Contoso.Example.v2");
Console.Write("Enter input: ");
string input = Console.ReadLine();

// protect the payload


string protectedPayload = protector.Protect(input);
Console.WriteLine($"Protect returned: {protectedPayload}");

// unprotect the payload


string unprotectedPayload = protector.Unprotect(protectedPayload);
Console.WriteLine($"Unprotect returned: {unprotectedPayload}");
}
}

TIP
Instancias de IDataProtectionProvider y IDataProtector son seguras para subprocesos para varios de los llamadores.
Se ha diseñado que una vez que un componente obtiene una referencia a un IDataProtector mediante una llamada a
CreateProtector , utilizará dicha referencia para varias llamadas a Protect y Unprotect . Una llamada a Unprotect
iniciará CryptographicException si no se puede comprobar o descifrar la carga protegida. Algunos componentes que desee
omitir los errores durante la desproteger operaciones; un componente que lee las cookies de autenticación puede controlar
este error y tratar la solicitud como si no tuviera en absoluto ninguna cookie en lugar de producirá un error en la solicitud
directamente. Los componentes que desea este comportamiento deben detectar específicamente CryptographicException
en lugar de admitir todas las excepciones.
Cadenas de propósito en ASP.NET Core
22/06/2018 • 6 minutes to read • Edit Online

Componentes que consumen IDataProtectionProvider debe pasar un único fines parámetro para el
CreateProtector método. Los fines parámetro es inherente a la seguridad del sistema de protección de datos,
ya que proporciona aislamiento entre los consumidores de cifrado, incluso si las claves criptográficas de raíz son
los mismos.
Cuando un consumidor especifica un propósito, la cadena de fin se usa junto con las claves criptográficas de
raíz para derivar criptográficas subclaves únicas para ese consumidor. Esto permite aislar el consumidor de
todos los otros consumidores de cifrado en la aplicación: ningún otro componente puede leer sus cargas y no se
puede leer cargas de cualquier otro componente. Este aislamiento también presenta factible todas las categorías
de un ataque contra el componente.

En el diagrama anterior, IDataProtector instancias A y B no leer de todas las demás cargas, solo sus propios.
La cadena de fin no tiene que ser secreto. Simplemente debe ser único en el sentido de que ningún otro
componente con buen comportamiento nunca proporcione la misma cadena de fin.

TIP
Con el nombre de espacio de nombres y el tipo del componente consumir las API de protección de datos es una buena
regla general, al igual que en la práctica que esta información nunca estará en conflicto.
Un componente creado por Contoso que es responsable de minting tokens de portador puede usar
Contoso.Security.BearerToken como cadena de su propósito. O - incluso mejor -, podría usar
Contoso.Security.BearerToken.v1 como cadena de su propósito. Anexar el número de versión permite que una versión
futura usar Contoso.Security.BearerToken.v2 como su propósito, y las distintas versiones sería completamente aisladas
entre sí como ir de cargas.

Desde el parámetro de fines CreateProtector es una matriz de cadenas, los pasos anteriores se haya en su
lugar especificados como [ "Contoso.Security.BearerToken", "v1" ] . Esto permite establecer una jerarquía de
propósitos y se abre la posibilidad de escenarios de varios inquilinos con el sistema de protección de datos.
WARNING
Componentes no deben permitir proporcionados por el usuario de confianza como el único origen de entrada de la
cadena con fines.
Por ejemplo, considere la posibilidad de un componente Contoso.Messaging.SecureMessage que es responsable de
almacenar mensajes seguros. Si el componente de mensajería seguro llamase a CreateProtector([ username ]) , a
continuación, un usuario malintencionado podría crear una cuenta con el nombre de usuario
"Contoso.Security.BearerToken" en un intento de obtener el componente para llamar a
CreateProtector([ "Contoso.Security.BearerToken" ]) , lo que sin darse cuenta y la mensajería segura sistema que
lleva cargas que se puede percibir como tokens de autenticación.
Una cadena con fines mejor para el componente de mensajería sería
CreateProtector([ "Contoso.Messaging.SecureMessage", "User: username" ]) , lo que proporciona aislamiento
adecuado.

El aislamiento que ofrecen y los comportamientos de IDataProtectionProvider , IDataProtector , y con fines de


son los siguientes:
Para un determinado IDataProtectionProvider objeto, el CreateProtector método creará una
IDataProtector objeto ligada a ambos en modo exclusivo la IDataProtectionProvider objeto que lo creó
y el parámetro de propósitos que se pasó al método.
El parámetro de fin no debe ser null. (Si no se especifica con fines como una matriz, esto significa que la
matriz no debe ser de longitud cero y todos los elementos de la matriz deben ser distinto de null). Un
propósito de una cadena vacía es técnicamente válido pero en absoluto.
Argumentos de dos objetivos son equivalentes si y solo si contienen las mismas cadenas (usando a un
comparador ordinal) en el mismo orden. Un argumento único propósito es equivalente a la matriz con
fines de solo elemento correspondiente.
Dos IDataProtector objetos son equivalentes si y solo si se crean desde equivalente
IDataProtectionProvider objetos con los parámetros de fines equivalente.

Para un determinado IDataProtector (objeto), una llamada a Unprotect(protectedData) devolverá el


original unprotectedData si y solo si protectedData := Protect(unprotectedData) para un equivalente
IDataProtector objeto.

NOTE
No estamos pensando en el caso de que algún componente intencionadamente elige una cadena de propósito que se
sabe que entran en conflicto con otro componente. Esos componentes básicamente se consideraría malintencionado, y
este sistema no está diseñado para proporcionar garantías de seguridad en caso de que código malintencionado ya se
está ejecutando dentro del proceso de trabajo.
Jerarquía de propósito y la arquitectura multiempresa
en ASP.NET Core
22/06/2018 • 3 minutes to read • Edit Online

Puesto que un IDataProtector también es implícitamente una IDataProtectionProvider , con fines se pueden
encadenar juntas. En este sentido, provider.CreateProtector([ "purpose1", "purpose2" ]) es equivalente a
provider.CreateProtector("purpose1").CreateProtector("purpose2") .

Esto permite algunas relaciones jerárquicas interesantes a través del sistema de protección de datos. En el ejemplo
anterior de Contoso.Messaging.SecureMessage, puede llamar el componente SecureMessage
provider.CreateProtector("Contoso.Messaging.SecureMessage") inicial una vez y almacenar en caché el resultado en
privado _myProvide campo. Protectores futuras, a continuación, pueden crearse a través de llamadas a
_myProvider.CreateProtector("User: username") , y se utilizan estos protectores para proteger los mensajes
individuales.
Esto también se voltea. Considere la posibilidad de una sola aplicación lógica qué hosts de varios inquilinos (un
CMS parece razonable) y cada inquilino pueden configurarse con su propio sistema de administración de
autenticación y el estado. La aplicación aglutina tiene un proveedor de maestro único y llama
provider.CreateProtector("Tenant 1") y provider.CreateProtector("Tenant 2") para proporcionar a cada inquilino
de su propio segmento aislado del sistema de protección de datos. Los inquilinos, a continuación, pudieron derivar
sus propios protectores individuales según sus propias necesidades, pero con independencia de forma rígida
intentan no pueden crear protectores que estén en conflicto con otro inquilino en el sistema. Esto se representa
gráficamente las indicadas a continuación.

WARNING
Se supone el paraguas de controles de la aplicación qué API están disponibles a los inquilinos individuales y que los
inquilinos no pueden ejecutar código arbitrario en el servidor. Si un inquilino puede ejecutar código arbitrario, podrían
realizar la reflexión privada para interrumpir las garantías de aislamiento, o que simplemente podrían leer directamente el
material de claves principal y derivan de las subclaves lo desean.

El sistema de protección de datos utiliza realmente una ordenación de la arquitectura multiempresa en su


configuración de predeterminada de fábrica. De forma predeterminada el material de claves maestro se almacena
en la carpeta de perfil de usuario de la cuenta de proceso de trabajo (o el registro, para identidades de grupo de
aplicaciones de IIS ). Pero es realmente bastante habitual usar una única cuenta para ejecutar varias aplicaciones y,
por tanto, todas estas aplicaciones se terminen compartiendo al maestro de material de claves. Para resolver este
problema, el sistema de protección de datos inserta automáticamente un identificador único por la aplicación
como el primer elemento de la cadena de propósito general. Ello implícita sirve para aislar las aplicaciones
individuales entre sí al tratar de forma eficaz cada aplicación como un único inquilino dentro del sistema y el
proceso de creación de protector parece idéntico a la imagen anterior.
Contraseñas de hash en ASP.NET Core
22/06/2018 • 2 minutes to read • Edit Online

La base de código de protección de datos incluye un paquete Microsoft.AspNetCore.Cryptography.KeyDerivation


que contiene funciones de derivación de claves criptográficas. Este paquete es un componente independiente y no
tiene ninguna dependencia en el resto del sistema de protección de datos. Se puede utilizar completamente por
separado. El origen coexista con el código de protección de datos base para su comodidad.
Actualmente, el paquete ofrece un método KeyDerivation.Pbkdf2 que permite que se aplican algoritmos hash a
una contraseña mediante el PBKDF2 algoritmo. Esta API es muy similar a existente de .NET Framework
Rfc2898DeriveBytes tipo, pero hay tres diferencias importantes:
1. El método compatible con el uso de varios PRFs (actualmente
KeyDerivation.Pbkdf2 HMACSHA1 ,
HMACSHA256 , y HMACSHA512 ), mientras que la Rfc2898DeriveBytes escriba sólo admite HMACSHA1 .
2. El KeyDerivation.Pbkdf2 método detecta el sistema operativo actual e intenta elegir la implementación de
la rutina, proporcionando un rendimiento mucho mejor en ciertos casos más optimizada. (En Windows 8,
que ofrece aproximadamente 10 veces el rendimiento de Rfc2898DeriveBytes .)
3. El KeyDerivation.Pbkdf2 método requiere que el llamador especificar todos los parámetros (sal, PRF y
número de iteraciones). El Rfc2898DeriveBytes tipo proporciona valores predeterminados para estos.
using System;
using System.Security.Cryptography;
using Microsoft.AspNetCore.Cryptography.KeyDerivation;

public class Program


{
public static void Main(string[] args)
{
Console.Write("Enter a password: ");
string password = Console.ReadLine();

// generate a 128-bit salt using a secure PRNG


byte[] salt = new byte[128 / 8];
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(salt);
}
Console.WriteLine($"Salt: {Convert.ToBase64String(salt)}");

// derive a 256-bit subkey (use HMACSHA1 with 10,000 iterations)


string hashed = Convert.ToBase64String(KeyDerivation.Pbkdf2(
password: password,
salt: salt,
prf: KeyDerivationPrf.HMACSHA1,
iterationCount: 10000,
numBytesRequested: 256 / 8));
Console.WriteLine($"Hashed: {hashed}");
}
}

/*
* SAMPLE OUTPUT
*
* Enter a password: Xtw9NMgx
* Salt: NZsP6NnmfBuYeJrrAKNuVQ==
* Hashed: /OOoOer10+tGwTRDTrQSoeCxVTFr6dtYly7d0cPxIak=
*/

Vea el código fuente de ASP.NET Core Identity PasswordHasher caso de uso de tipo para un mundo real.
Limite la duración de las cargas protegidas en
ASP.NET Core
22/06/2018 • 5 minutes to read • Edit Online

Existen escenarios donde desea que el desarrollador de aplicaciones para crear una carga protegida que expira
tras un período de tiempo establecido. Por ejemplo, la carga protegida podría representar un token de
restablecimiento de contraseña que solo debe ser válido durante una hora. Es posible para el desarrollador puede
crear su propio formato de carga que contiene una fecha de expiración incrustados, y los desarrolladores
avanzados que desee hacerlo de todos modos, pero para la mayoría de los desarrolladores administrar estos
caducidad puede crecer tedioso.
Para facilitar esta tarea para nuestros clientes de desarrollador, el paquete
Microsoft.AspNetCore.DataProtection.Extensions contiene las API de la utilidad para crear cargas de expiran
automáticamente tras un período de tiempo establecido. Estas API de bloqueo de la ITimeLimitedDataProtector
tipo.

Uso de la API
El ITimeLimitedDataProtector es la interfaz principal para proteger y desproteger cargas de tiempo limitado /
expiración automática. Para crear una instancia de un ITimeLimitedDataProtector , primero necesitará una instancia
de una normal IDataProtector construido con un propósito específico. Una vez el IDataProtector instancia esté
disponible, llame a la IDataProtector.ToTimeLimitedDataProtector método de extensión para devolver un protector
con capacidades integradas de expiración.
ITimeLimitedDataProtector expone los siguientes métodos de superficie y extensión de API:
CreateProtector (propósito de cadena): ITimeLimitedDataProtector - esta API es similar a la existente
IDataProtectionProvider.CreateProtector en que se puede utilizar para crear finalidad cadenas desde un
protector de tiempo limitado de raíz.
Proteger (byte [] texto simple, expiración de DateTimeOffset): byte]
Proteger (texto simple de byte [], duración del intervalo de tiempo): byte]
Proteger (como texto simple de byte []): byte]
Proteger (texto simple de cadena, expiración de DateTimeOffset): cadena
Proteger (texto simple de cadena, duración del intervalo de tiempo): cadena
Proteger (texto simple de la cadena): cadena
Además de los principales Protect métodos que toman sólo el texto no cifrado, hay nuevas sobrecargas que
permiten especificar la fecha de expiración de la carga. La fecha de expiración se puede especificar como una fecha
absoluta (a través de un DateTimeOffset ) o como una hora relativa (desde el sistema actual time y a través de un
TimeSpan ). Si se llama a una sobrecarga que no toma una expiración, se supone la carga nunca a punto de expirar.

Desproteger (byte [] protectedData, out expiración DateTimeOffset): byte]


Desproteger (byte [] protectedData): byte]
Desproteger (cadena protectedData, out expiración DateTimeOffset): cadena
Desproteger (cadena protectedData): cadena
El Unprotect métodos devuelven los datos protegidos originales. Si aún no ha expirado la carga, la expiración
absoluta se devuelve como un parámetro junto con los datos protegidos originales de salida opcionales. Si la
carga ha expirado, todas las sobrecargas del método Unprotect producirá CryptographicException.

WARNING
No se recomienda usar estas API para proteger las cargas que requieren la persistencia a largo plazo o indefinida.
"¿Permitirme para que las cargas protegidas ser irrecuperables permanentemente después de un mes?" puede actuar como
una buena regla general; Si la respuesta es no a los desarrolladores a continuación, deben considerar las API alternativas.

El ejemplo siguiente utiliza la las rutas de código no DI para crear instancias del sistema de protección de datos.
Para ejecutar este ejemplo, asegúrese de que ha agregado una referencia al paquete
Microsoft.AspNetCore.DataProtection.Extensions en primer lugar.

using System;
using System.IO;
using System.Threading;
using Microsoft.AspNetCore.DataProtection;

public class Program


{
public static void Main(string[] args)
{
// create a protector for my application

var provider = DataProtectionProvider.Create(new DirectoryInfo(@"c:\myapp-keys\"));


var baseProtector = provider.CreateProtector("Contoso.TimeLimitedSample");

// convert the normal protector into a time-limited protector


var timeLimitedProtector = baseProtector.ToTimeLimitedDataProtector();

// get some input and protect it for five seconds


Console.Write("Enter input: ");
string input = Console.ReadLine();
string protectedData = timeLimitedProtector.Protect(input, lifetime: TimeSpan.FromSeconds(5));
Console.WriteLine($"Protected data: {protectedData}");

// unprotect it to demonstrate that round-tripping works properly


string roundtripped = timeLimitedProtector.Unprotect(protectedData);
Console.WriteLine($"Round-tripped data: {roundtripped}");

// wait 6 seconds and perform another unprotect, demonstrating that the payload self-expires
Console.WriteLine("Waiting 6 seconds...");
Thread.Sleep(6000);
timeLimitedProtector.Unprotect(protectedData);
}
}

/*
* SAMPLE OUTPUT
*
* Enter input: Hello!
* Protected data: CfDJ8Hu5z0zwxn...nLk7Ok
* Round-tripped data: Hello!
* Waiting 6 seconds...
* <<throws CryptographicException with message 'The payload expired at ...'>>

*/
Desproteger cargas cuyas claves se han revocado en
ASP.NET Core
22/06/2018 • 5 minutes to read • Edit Online

La API de protección de datos de ASP.NET Core no están pensados principalmente para la persistencia indefinido
de cargas confidenciales. Otras tecnologías, como Windows CNG DPAPI y Azure Rights Management son más
adecuados para el escenario de almacenamiento indefinido, y tienen capacidades de administración de claves
seguro según corresponda. Es decir, no hay nada prohibir a un desarrollador usa las API de protección de datos de
ASP.NET Core para la protección a largo plazo de información confidencial. Claves nunca se eliminan desde el
anillo de clave, por lo que IDataProtector.Unprotect siempre se puede recuperar cargas existentes siempre que las
claves sean disponible y es válido.
Sin embargo, un problema se produce cuando el programador intenta desproteger los datos que se ha protegido
con una clave revocada, como IDataProtector.Unprotect se iniciará una excepción en este caso. Esto podría ser
bien para las cargas breves o temporales (por ejemplo, tokens de autenticación), tal y como fácilmente se pueden
volver a crear estos tipos de cargas por el sistema y en el peor el visitante del sitio podría ser necesario volver a
iniciar sesión. Pero para cargas persistentes, tener Unprotect throw podría provocar la pérdida de datos aceptable.

IPersistedDataProtector
Para admitir el escenario de permitir cargas que se debe desproteger incluso de cara a revocados claves, el
sistema de protección de datos contiene un IPersistedDataProtector tipo. Para obtener una instancia de
IPersistedDataProtector , simplemente obtener una instancia de IDataProtector en el modo normal y conversión
de try el IDataProtector a IPersistedDataProtector .

NOTE
No todos los IDataProtector instancias pueden convertirse a IPersistedDataProtector . Los programadores deben
utilizar el C# como operador o similar evitar las excepciones en tiempo de ejecución deberse a conversiones no válidas y que
deben estar preparado para controlar el caso de fallo adecuadamente.

IPersistedDataProtector expone la superficie de API siguiente:

DangerousUnprotect(byte[] protectedData, bool ignoreRevocationErrors,


out bool requiresMigration, out bool wasRevoked) : byte[]

Esta API toma la carga protegida (como una matriz de bytes) y devuelve la carga sin protección. No hay ninguna
sobrecarga basada en cadena. Los dos parámetros de salida son los siguientes.
requiresMigration : se establece en true si la clave utilizada para proteger esta carga ya no es la clave
predeterminada activa, por ejemplo, la clave utilizada para proteger esta carga es antigua y tiene una
operación de reversión de clave desde tendrán lugar. El llamador puede llamar a considere la posibilidad de
volver a proteger la carga de la función de sus necesidades empresariales.
wasRevoked : se establecerá en true si la clave utilizada para proteger esta carga se ha revocado.
WARNING
Debe extremar las precauciones al pasar ignoreRevocationErrors: true a la DangerousUnprotect método. Si después
de llamar a este método la wasRevoked valor es true, a continuación, la clave utilizada para proteger esta carga ha sido
revocada y autenticidad de la carga debe tratarse como sospechosa. En este caso, solo seguir funcionando en la carga sin
protección si tienen seguridad independiente que es auténtica, por ejemplo, que procede de una base de datos segura en
lugar de enviarse por un cliente web no es de confianza.

using System;
using System.IO;
using System.Text;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.DataProtection.KeyManagement;
using Microsoft.Extensions.DependencyInjection;

public class Program


{
public static void Main(string[] args)
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddDataProtection()
// point at a specific folder and use DPAPI to encrypt keys
.PersistKeysToFileSystem(new DirectoryInfo(@"c:\temp-keys"))
.ProtectKeysWithDpapi();
var services = serviceCollection.BuildServiceProvider();

// get a protector and perform a protect operation


var protector = services.GetDataProtector("Sample.DangerousUnprotect");
Console.Write("Input: ");
byte[] input = Encoding.UTF8.GetBytes(Console.ReadLine());
var protectedData = protector.Protect(input);
Console.WriteLine($"Protected payload: {Convert.ToBase64String(protectedData)}");

// demonstrate that the payload round-trips properly


var roundTripped = protector.Unprotect(protectedData);
Console.WriteLine($"Round-tripped payload: {Encoding.UTF8.GetString(roundTripped)}");

// get a reference to the key manager and revoke all keys in the key ring
var keyManager = services.GetService<IKeyManager>();
Console.WriteLine("Revoking all keys in the key ring...");
keyManager.RevokeAllKeys(DateTimeOffset.Now, "Sample revocation.");

// try calling Protect - this should throw


Console.WriteLine("Calling Unprotect...");
try
{
var unprotectedPayload = protector.Unprotect(protectedData);
Console.WriteLine($"Unprotected payload: {Encoding.UTF8.GetString(unprotectedPayload)}");
}
catch (Exception ex)
{
Console.WriteLine($"{ex.GetType().Name}: {ex.Message}");
}

// try calling DangerousUnprotect


Console.WriteLine("Calling DangerousUnprotect...");
try
{
IPersistedDataProtector persistedProtector = protector as IPersistedDataProtector;
if (persistedProtector == null)
{
throw new Exception("Can't call DangerousUnprotect.");
}

bool requiresMigration, wasRevoked;


bool requiresMigration, wasRevoked;
var unprotectedPayload = persistedProtector.DangerousUnprotect(
protectedData: protectedData,
ignoreRevocationErrors: true,
requiresMigration: out requiresMigration,
wasRevoked: out wasRevoked);
Console.WriteLine($"Unprotected payload: {Encoding.UTF8.GetString(unprotectedPayload)}");
Console.WriteLine($"Requires migration = {requiresMigration}, was revoked = {wasRevoked}");
}
catch (Exception ex)
{
Console.WriteLine($"{ex.GetType().Name}: {ex.Message}");
}
}
}

/*
* SAMPLE OUTPUT
*
* Input: Hello!
* Protected payload: CfDJ8LHIzUCX1ZVBn2BZ...
* Round-tripped payload: Hello!
* Revoking all keys in the key ring...
* Calling Unprotect...
* CryptographicException: The key {...} has been revoked.
* Calling DangerousUnprotect...
* Unprotected payload: Hello!
* Requires migration = True, was revoked = True
*/
Configuración de la protección de datos en ASP.NET
Core
21/06/2018 • 2 minutes to read • Edit Online

Consulte estos temas para obtener información sobre la configuración de la protección de datos en ASP.NET Core:
Configurar la protección de datos en ASP.NET Core
Una introducción a la configuración de la protección de datos de ASP.NET Core.
Administración y duración de las claves de protección de datos
Información sobre la administración y duración de las claves de protección de datos.
Compatibilidad con la directiva a nivel de equipo para la protección de datos
Obtenga más información sobre cómo configurar una directiva predeterminada a nivel de equipo para
todas las aplicaciones que usan la protección de datos.
Escenarios no compatibles con DI para la protección de datos en ASP.NET Core
Cómo usar el tipo concreto DataProtectionProvider para usar la protección de datos sin tener que pasar a
través de rutas de código específicas de DI.
Configurar la protección de datos de ASP.NET
Core
22/06/2018 • 18 minutes to read • Edit Online

Por Rick Anderson


Cuando se inicializa el sistema de protección de datos, se aplica configuración predeterminada basado en el
entorno operativo. Esta configuración es suelen ser adecuada para aplicaciones que se ejecutan en un único
equipo. Hay casos donde un desarrollador puede cambiar la configuración predeterminada:
La aplicación se reparte entre varias máquinas.
Por motivos de cumplimiento.
En estos casos, el sistema de protección de datos ofrece una API enriquecida de configuración.

WARNING
De forma similar a los archivos de configuración, el anillo de clave de protección de datos deben protegerse utilizando
los permisos adecuados. Puede elegir cifrar las claves en reposo, pero esto no evita que los atacantes de crear nuevas
claves. Por lo tanto, la seguridad de la aplicación se ve afectado. La ubicación de almacenamiento configurada con
protección de datos debe tener su acceso limitado a la propia aplicación similar a la manera en que protege a los
archivos de configuración. Por ejemplo, si decide almacenar el anillo de clave en el disco, utilice permisos del sistema de
archivos. Asegurar solo la identidad en que se ejecuta la aplicación web se lectura, escritura y crear el acceso a ese
directorio. Si utiliza almacenamiento de tabla de Azure, solo la aplicación web debe tener la capacidad de leer, escribir o
crear nuevas entradas en el almacén de tablas, etcetera.
El método de extensión AddDataProtection devuelve un IDataProtectionBuilder. IDataProtectionBuilder expone
métodos de extensión que se pueden encadenar juntos para configurar la protección de datos de opciones.

ProtectKeysWithAzureKeyVault
Para almacenar las claves en el almacén de claves de Azure, configurar el sistema con
ProtectKeysWithAzureKeyVault en la Startup clase:

public void ConfigureServices(IServiceCollection services)


{
services.AddDataProtection()
.PersistKeysToAzureBlobStorage(new Uri("<blobUriWithSasToken>"))
.ProtectKeysWithAzureKeyVault("<keyIdentifier>", "<clientId>", "<clientSecret>");
}

Establecer la ubicación de almacenamiento de anillo de clave (por ejemplo, PersistKeysToAzureBlobStorage).


La ubicación debe establecerse porque una llamada a ProtectKeysWithAzureKeyVault implementa un
IXmlEncryptor que deshabilita la configuración de protección automática de los datos, incluida la ubicación de
almacenamiento de anillo de clave. El ejemplo anterior utiliza almacenamiento de blobs de Azure para
conservar el anillo de clave. Para obtener más información, consulte proveedores de almacenamiento de
claves: Azure y Redis. También puede conservar el anillo de clave localmente con PersistKeysToFileSystem.
El keyIdentifier es el identificador de clave de almacén de claves usado para el cifrado de clave (por
ejemplo, https://contosokeyvault.vault.azure.net/keys/dataprotection/ ).
ProtectKeysWithAzureKeyVault sobrecargas:
ProtectKeysWithAzureKeyVault (IDataProtectionBuilder, KeyVaultClient, String) permite el uso de un
KeyVaultClient para habilitar el sistema de protección de datos usar el almacén de claves.
ProtectKeysWithAzureKeyVault (IDataProtectionBuilder, cadena, cadena, X509Certificate2) permite el uso
de un ClientId y X509Certificate para habilitar el sistema de protección de datos usar el almacén de
claves.
ProtectKeysWithAzureKeyVault (IDataProtectionBuilder, String, String, String) permite el uso de un
ClientId y ClientSecret para habilitar el sistema de protección de datos usar el almacén de claves.

PersistKeysToFileSystem
Para almacenar las claves en un recurso compartido UNC en lugar de en el % LOCALAPPDATA % ubicación
predeterminada, configure el sistema con PersistKeysToFileSystem:

public void ConfigureServices(IServiceCollection services)


{
services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(@"\\server\share\directory\"));
}

WARNING
Si cambia la ubicación de persistencia de clave, el sistema cifra ya no automáticamente claves en reposo, ya que no sabe
si DPAPI es un mecanismo de cifrado adecuado.

ProtectKeysWith*
Puede configurar el sistema para proteger las claves en reposo mediante una llamada a cualquiera de los
ProtectKeysWith* API de configuración. Tenga en cuenta el ejemplo siguiente, que almacena las claves en un
recurso compartido UNC y cifra estas claves en reposo con un certificado X.509 concreto:

public void ConfigureServices(IServiceCollection services)


{
services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(@"\\server\share\directory\"))
.ProtectKeysWithCertificate("thumbprint");
}

Vea clave de cifrado de datos almacenados para obtener más ejemplos e información sobre los mecanismos
de cifrado de clave integrado.

SetDefaultKeyLifetime
Para configurar el sistema para usar una vigencia de clave de 14 días en lugar del predeterminado de 90 días,
use SetDefaultKeyLifetime:

public void ConfigureServices(IServiceCollection services)


{
services.AddDataProtection()
.SetDefaultKeyLifetime(TimeSpan.FromDays(14));
}
SetApplicationName
De forma predeterminada, el sistema de protección de datos aísla aplicaciones entre sí, incluso si comparte el
mismo repositorio clave físico. Esto evita que las aplicaciones de descripción de todas las demás cargas
protegido. Para compartir protegidos cargas entre las dos aplicaciones, use SetApplicationName con el
mismo valor para cada aplicación:

public void ConfigureServices(IServiceCollection services)


{
services.AddDataProtection()
.SetApplicationName("shared app name");
}

DisableAutomaticKeyGeneration
Puede que tenga un escenario donde no desea que una aplicación para revertir automáticamente las claves
(crear nuevas claves) tal y como se aproximen a expiración. Un ejemplo de esto podría ser aplicaciones
configuradas en una relación principal/secundario, donde solo la aplicación principal es responsable de
problemas de administración de claves y aplicaciones secundarias solo tienen una vista de solo lectura del
anillo de clave. Las aplicaciones secundarias pueden configurarse para tratar el anillo de clave como de solo
lectura configurando el sistema con DisableAutomaticKeyGeneration:

public void ConfigureServices(IServiceCollection services)


{
services.AddDataProtection()
.DisableAutomaticKeyGeneration();
}

Aislamiento por aplicación


Cuando el sistema de protección de datos se proporciona un host de ASP.NET Core, aísla automáticamente
aplicaciones entre sí, incluso si esas aplicaciones se ejecuten en la misma cuenta de proceso de trabajo y están
usando el mismo material de clave maestro. Esto es similar al modificador IsolateApps de System.Web
<machineKey > elemento.
El mecanismo de aislamiento funciona teniendo en cuenta cada aplicación en el equipo local como un único
inquilino, por lo tanto la IDataProtector para cualquier aplicación determinada incluye automáticamente el
identificador de aplicación como un discriminador de la raíz. Identificador único de la aplicación procede de
uno de estos dos lugares:
1. Si la aplicación se hospeda en IIS, el identificador único es la ruta de acceso de configuración de la
aplicación. Si una aplicación se implementa en un entorno de granja de servidores web, este valor
debería ser estable suponiendo que los entornos de IIS están configurados de forma similar en todos
los equipos de la granja de servidores web.
2. Si la aplicación no está hospedada en IIS, el identificador único es la ruta de acceso física de la
aplicación.
El identificador único está diseñado para sobrevivir restablece — tanto de la aplicación individual de la propia
máquina.
Este mecanismo de aislamiento se da por supuesto que las aplicaciones no son malintencionadas. Una
aplicación malintencionada siempre puede afectar a cualquier otra aplicación que se ejecuta en la misma
cuenta de proceso de trabajo. En un entorno de hospedaje compartido donde las aplicaciones no son de
confianza mutua, el proveedor de hospedaje debe tomar medidas para garantizar el aislamiento de nivel de
sistema operativo entre aplicaciones, incluida la separación de las aplicaciones subyacentes repositorios de
clave.
Si el sistema de protección de datos no se proporcionó un host de ASP.NET Core (por ejemplo, si crea
instancias de él a través de la DataProtectionProvider tipo concreto) se deshabilita el aislamiento de la
aplicación de forma predeterminada. Cuando se deshabilita el aislamiento de la aplicación, respaldadas por el
mismo material de claves de todas las aplicaciones pueden compartir cargas mientras proporcionan
adecuado fines. Para proporcionar aislamiento de aplicaciones en este entorno, llame a la
SetApplicationName método en la configuración de objeto y proporcione un nombre único para cada
aplicación.

Cambiar algoritmos con UseCryptographicAlgorithms


La pila de protección de datos permite cambiar el algoritmo predeterminado usado por claves recién
generado. La manera más sencilla de hacerlo consiste en llamar a UseCryptographicAlgorithms de la
devolución de llamada de configuración:
ASP.NET Core 2.x
ASP.NET Core 1.x

services.AddDataProtection()
.UseCryptographicAlgorithms(
new AuthenticatedEncryptorConfiguration()
{
EncryptionAlgorithm = EncryptionAlgorithm.AES_256_CBC,
ValidationAlgorithm = ValidationAlgorithm.HMACSHA256
});

El algoritmo de cifrado predeterminada es AES -256-CBC y el valor predeterminado ValidationAlgorithm es


HMACSHA256. La directiva predeterminada se puede establecer un administrador del sistema a través de un
todo el equipo directiva, pero una llamada explícita a UseCryptographicAlgorithms invalida la directiva
predeterminada.
Al llamar a UseCryptographicAlgorithms le permite especificar el algoritmo deseado de una lista predefinida
de integrados. No tiene que preocuparse acerca de la implementación del algoritmo. En el escenario anterior,
el sistema de protección de datos intenta usar la implementación de CNG de AES si se ejecuta en Windows.
En caso contrario, vuelve a los recursos administrados System.Security.Cryptography.Aes clase.
Puede especificar manualmente una implementación a través de una llamada a
UseCustomCryptographicAlgorithms.

TIP
Algoritmos de cambio no afecta a las claves existentes en el anillo de clave. Solo afecta a las claves recién generado.

Especificar algoritmos administrados personalizados


ASP.NET Core 2.x
ASP.NET Core 1.x
Para especificar algoritmos administrados personalizados, cree una
ManagedAuthenticatedEncryptorConfiguration instancia que señala a los tipos de implementación:
serviceCollection.AddDataProtection()
.UseCustomCryptographicAlgorithms(
new ManagedAuthenticatedEncryptorConfiguration()
{
// A type that subclasses SymmetricAlgorithm
EncryptionAlgorithmType = typeof(Aes),

// Specified in bits
EncryptionAlgorithmKeySize = 256,

// A type that subclasses KeyedHashAlgorithm


ValidationAlgorithmType = typeof(HMACSHA256)
});

Por lo general el *propiedades de tipo deben apuntar a concreto, las implementaciones (a través de un
constructor sin parámetros público) se pueden crear instancias de SymmetricAlgorithm y
KeyedHashAlgorithm, aunque el casos de especial del sistema, como algunos valores typeof(Aes) para su
comodidad.

NOTE
El SymmetricAlgorithm debe tener una longitud de clave de 128 bits ≥ y un tamaño de bloque de 64 bits ≥ y debe
admitir el cifrado del modo CBC con relleno PKCS #7. El KeyedHashAlgorithm debe tener un tamaño de síntesis de > =
128 bits, y debe ser compatible con claves de una longitud igual a la longitud de texto implícita del algoritmo de hash.
El KeyedHashAlgorithm no es estrictamente necesario que sea HMAC.

Especificar algoritmos personalizados de CNG de Windows


ASP.NET Core 2.x
ASP.NET Core 1.x
Para especificar un algoritmo CNG de Windows personalizado mediante el cifrado de modo CBC con
validación de HMAC, cree un CngCbcAuthenticatedEncryptorConfiguration instancia que contiene la
información de algoritmo:

services.AddDataProtection()
.UseCustomCryptographicAlgorithms(
new CngCbcAuthenticatedEncryptorConfiguration()
{
// Passed to BCryptOpenAlgorithmProvider
EncryptionAlgorithm = "AES",
EncryptionAlgorithmProvider = null,

// Specified in bits
EncryptionAlgorithmKeySize = 256,

// Passed to BCryptOpenAlgorithmProvider
HashAlgorithm = "SHA256",
HashAlgorithmProvider = null
});
NOTE
El algoritmo de cifrado de bloques simétrico debe tener una longitud de clave de > = 128 bits, un tamaño de bloque
de > = 64 bits, y debe admitir el cifrado del modo CBC con relleno PKCS #7. El algoritmo hash debe tener un tamaño
de síntesis de > = 128 bits y debe ser compatible con que se abre con la BCRYPT_ALG_controlar_HMAC_indicador de
marca. El *propiedades del proveedor se pueden establecer en null para utilizar el proveedor predeterminado para el
algoritmo especificado. Consulte la BCryptOpenAlgorithmProvider documentación para obtener más información.

ASP.NET Core 2.x


ASP.NET Core 1.x
Para especificar un algoritmo CNG de Windows personalizado mediante el cifrado del modo de
Galois/contador con la validación, cree un CngGcmAuthenticatedEncryptorConfiguration instancia que
contiene la información de algoritmo:

services.AddDataProtection()
.UseCustomCryptographicAlgorithms(
new CngGcmAuthenticatedEncryptorConfiguration()
{
// Passed to BCryptOpenAlgorithmProvider
EncryptionAlgorithm = "AES",
EncryptionAlgorithmProvider = null,

// Specified in bits
EncryptionAlgorithmKeySize = 256
});

NOTE
El algoritmo de cifrado de bloques simétrico debe tener una longitud de clave de > = 128 bits, un tamaño de bloque
de 128 bits exactamente, y debe ser compatible con cifrado de GCM. Puede establecer la EncryptionAlgorithmProvider
propiedad en null para utilizar el proveedor predeterminado para el algoritmo especificado. Consulte la
BCryptOpenAlgorithmProvider documentación para obtener más información.

Especificar otros algoritmos personalizados


Aunque no se expone como una API de primera clase, el sistema de protección de datos es lo suficientemente
extensible para permitir la especificación de casi cualquier tipo de algoritmo. Por ejemplo, es posible
mantener todas las claves contenidas dentro de un módulo de seguridad de Hardware (HSM ) y para
proporcionar una implementación personalizada del núcleo de rutinas de cifrado y descifrado. Vea
IAuthenticatedEncryptor en principales de extensibilidad de criptografía para obtener más información.

Claves de persistencia cuando se hospedan en un contenedor de


Docker
Cuando se hospedan en un Docker contenedor, las claves deben mantenerse en la vista:
Una carpeta que es un volumen de Docker que se conserva más allá de la duración del contenedor, como
un volumen compartido o un volumen montado de host.
Un proveedor externo, como el almacén de claves de Azure o Redis.

Vea también
Escenarios no compatibles con DI
Directiva para toda la máquina
Administración de claves de protección de datos y la
duración en ASP.NET Core
22/06/2018 • 5 minutes to read • Edit Online

Por Rick Anderson

Administración de claves
La aplicación intenta detectar su entorno operativo y controlar la configuración de la clave por sí mismo.
1. Si la aplicación se hospeda en aplicaciones de Azure, las claves se conservan en el
%HOME%\ASP.NET\DataProtection-Keys carpeta. Esta carpeta está respaldada por el almacenamiento de
red y se sincroniza en todas las máquinas que hospedan la aplicación.
Las claves no están protegidas en reposo.
El DataProtection claves carpeta proporciona el anillo de clave a todas las instancias de una aplicación
en una única ranura de implementación.
Las ranuras de implementación independientes, por ejemplo, almacenamiento provisional y
producción, no comparten ningún anillo de clave. Al intercambiar las ranuras de implementación, por
ejemplo, el intercambio de ensayo a producción o usando A / B pruebas, cualquier aplicación con la
protección de datos no podrá descifrar los datos almacenados mediante el anillo de clave dentro de la
ranura anterior. Esto conduce al usuario que se haya iniciado sesión en una aplicación que usa la
autenticación con cookies ASP.NET Core estándar, porque usa protección de datos para proteger sus
cookies. Si desea llaveros independiente de la ranura, utiliza un proveedor de anillo de clave externa,
como almacenamiento de blobs de Azure, el almacén de claves de Azure, un almacén de SQL, o la
caché en Redis.
2. Si el perfil de usuario está disponible, las claves se conservan en el
%LOCALAPPDATA%\ASP.NET\DataProtection-Keys carpeta. Si el sistema operativo es Windows, las
claves se cifran en reposo con DPAPI.
3. Si la aplicación se hospeda en IIS, las claves se conservan en el registro HKLM en una clave del registro
especial que se disponen de las ACL solo a la cuenta de proceso de trabajo. Las claves se cifran en reposo
con DPAPI.
4. Si coincide con ninguna de estas condiciones, no se conservan las claves fuera del proceso actual. Cuando
el proceso se cierra, todos los genera claves se pierden.
El programador está siempre en control total y puede invalidar cómo y dónde se almacenan las claves. Las tres
primeras opciones anteriores deben proporcionar buenos valores predeterminados para la mayoría de las
aplicaciones similar a cómo ASP.NET <machineKey > rutinas de la generación automática funcionaban en el
pasado. La opción de reserva final es el único escenario que requiere que el desarrollador especificar
configuración por adelantado si quieren persistencia de clave, pero esta reserva sólo se produce en situaciones
excepcionales.
Cuando se hospedan en un contenedor de Docker, las claves deben conservarse en una carpeta que es un
volumen de Docker (un volumen compartido o un volumen montado de host que se conserva más allá de la
duración del contenedor) o en un proveedor externo, como el almacén de claves de Azure o Redis. Un proveedor
externo también es útil en escenarios de granja de servidores web si las aplicaciones no pueden obtener acceso a
un volumen compartido de red (consulte PersistKeysToFileSystem para obtener más información).
WARNING
Si el desarrollador invalida las reglas descritas anteriormente y señala el sistema de protección de datos en un repositorio
de clave específico, se deshabilita el cifrado automático de claves en reposo. Protección en rest puede habilitarse de nuevo
a través de configuración.

Vigencia de clave
Las claves tienen una duración de 90 días de forma predeterminada. Cuando expira una clave, la aplicación
genera una nueva clave automáticamente y establece la nueva clave como la clave activa. Claves retiradas
quedan en el sistema, siempre y cuando la aplicación puede descifrar los datos protegidos con ellos. Vea
administración de claves para obtener más información.

Algoritmos predeterminados
El algoritmo de protección de carga predeterminado usado es AES -256-CBC para HMACSHA256 la
confidencialidad y la autenticidad. Una clave maestra de 512 bits, cambiada cada 90 días, se utiliza para derivar
las claves secundarias dos utilizadas para estos algoritmos según una por cada carga. Vea subclave derivación
para obtener más información.

Vea también
Extensibilidad de administración de claves
Admite la directiva de todo el equipo de protección
de datos en ASP.NET Core
22/06/2018 • 7 minutes to read • Edit Online

Por Rick Anderson


Cuando se ejecuta en Windows, el sistema de protección de datos tiene compatibilidad limitada para establecer
una directiva de todo el equipo de forma predeterminada para todas las aplicaciones que utilizan protección de
datos de ASP.NET Core. La idea general es que un administrador puede que desee cambiar un valor
predeterminado, como los algoritmos utilizan o la vigencia de la clave, sin necesidad de actualizar manualmente
cada aplicación en el equipo.

WARNING
El administrador del sistema puede establecer la directiva predeterminada, pero no puede aplicarla. El desarrollador de
aplicaciones siempre puede reemplazar cualquier valor con uno de su propia elección. La directiva predeterminada sólo
afecta a las aplicaciones que el desarrollador no ha especificado un valor explícito para una configuración.

Establecer la directiva predeterminada


Para establecer una directiva de forma predeterminada, un administrador puede establecer los valores conocidos
en el registro del sistema en la siguiente clave del registro:
HKLM\SOFTWARE\Microsoft\DotNetPackages\Microsoft.AspNetCore.DataProtection
Si está en un sistema operativo de 64 bits y desea afectan al comportamiento de las aplicaciones de 32 bits,
recuerde que debe configurar el equivalente de Wow6432Node de la clave anterior.
Los valores admitidos se muestran a continuación.

VALOR TIPO DESCRIPCIÓN

EncryptionType cadena Especifica los algoritmos que se deben


usar para la protección de datos. El
valor debe ser CBC de CNG, GCM CNG
o administrado y se describe con más
detalle a continuación.

DefaultKeyLifetime DWORD Especifica la duración de claves recién


generado. El valor se especifica en días
y debe ser > = 7.

KeyEscrowSinks cadena Especifica los tipos que se usan para la


custodia de clave. El valor es una lista
delimitada por punto y coma de
receptores de custodia de clave, donde
cada elemento de la lista es el nombre
de ensamblado de un tipo que
implementa IKeyEscrowSink.

Tipos de cifrado
Si EncryptionType es CBC de CNG, el sistema está configurado para utilizar un cifrado por bloques simétrico
modo CBC para confidencialidad y HMAC para autenticidad con servicios proporcionados por Windows CNG
(vea especificar algoritmos personalizados de Windows CNG para más detalles). Se admiten los siguientes
valores adicionales, cada uno de los cuales corresponde a una propiedad en el tipo de
CngCbcAuthenticatedEncryptionSettings.

VALOR TIPO DESCRIPCIÓN

EncryptionAlgorithm cadena El nombre de un algoritmo de cifrado


de bloques simétrico entendido CNG.
Este algoritmo se abre en modo CBC.

EncryptionAlgorithmProvider cadena El nombre de la implementación del


proveedor CNG que puede generar el
algoritmo EncryptionAlgorithm.

EncryptionAlgorithmKeySize DWORD La longitud (en bits) de la clave para la


derivación para el algoritmo de cifrado
de bloques simétrico.

HashAlgorithm cadena El nombre de un algoritmo de hash


entendido CNG. Este algoritmo se abre
en modo HMAC.

HashAlgorithmProvider cadena El nombre de la implementación del


proveedor CNG que puede generar el
algoritmo HashAlgorithm.

Si EncryptionType es CNG GCM, el sistema está configurado para usar un cifrado por bloques simétrico modo
Galois/contador para la confidencialidad y la autenticidad con servicios proporcionados por Windows CNG (vea
especificar algoritmos personalizados de Windows CNG Para obtener más información.) Se admiten los
siguientes valores adicionales, cada uno de los cuales corresponde a una propiedad en el tipo de
CngGcmAuthenticatedEncryptionSettings.

VALOR TIPO DESCRIPCIÓN

EncryptionAlgorithm cadena El nombre de un algoritmo de cifrado


de bloques simétrico entendido CNG.
Este algoritmo se abre en modo de
Galois/contador.

EncryptionAlgorithmProvider cadena El nombre de la implementación del


proveedor CNG que puede generar el
algoritmo EncryptionAlgorithm.

EncryptionAlgorithmKeySize DWORD La longitud (en bits) de la clave para la


derivación para el algoritmo de cifrado
de bloques simétrico.

Si se administra EncryptionType, el sistema está configurado para utilizar un SymmetricAlgorithm administrado


para la confidencialidad y KeyedHashAlgorithm para autenticidad (vea especificar personalizado administrado
algoritmos para obtener más detalles). Se admiten los siguientes valores adicionales, cada uno de los cuales
corresponde a una propiedad en el tipo de ManagedAuthenticatedEncryptionSettings.
VALOR TIPO DESCRIPCIÓN

EncryptionAlgorithmType cadena El nombre calificado con el ensamblado


de un tipo que implementa
SymmetricAlgorithm.

EncryptionAlgorithmKeySize DWORD La longitud (en bits) de la clave para la


derivación para el algoritmo de cifrado
simétrico.

ValidationAlgorithmType cadena El nombre calificado con el ensamblado


de un tipo que implementa
KeyedHashAlgorithm.

Si EncryptionType tiene cualquier otro valor distinto de null o está vacío, el sistema de protección de datos
produce una excepción durante el inicio.

WARNING
Al configurar una configuración de directiva predeterminada que afecta a los nombres de tipo (EncryptionAlgorithmType,
ValidationAlgorithmType, KeyEscrowSinks), los tipos deben ser disponibles para la aplicación. Esto significa que para
aplicaciones que se ejecutan en CLR de escritorio, los ensamblados que contienen estos tipos deben estar presentes en la
caché de ensamblados Global (GAC). Para aplicaciones de ASP.NET Core que se ejecutan en .NET Core, deben instalarse los
paquetes que contienen estos tipos.
DI no compatible con escenarios para la protección
de datos en ASP.NET Core
22/06/2018 • 4 minutes to read • Edit Online

Por Rick Anderson


Suele ser el sistema de protección de datos de ASP.NET Core agregado a un contenedor de servicio y utilizado
por los componentes dependientes a través de la inserción de dependencias (DI). Sin embargo, hay casos donde
no es factible y deseadas, especialmente al importar el sistema en una aplicación existente.
Para admitir estos escenarios, el Microsoft.AspNetCore.DataProtection.Extensions paquete proporciona un tipo
concreto, DataProtectionProvider, que ofrece una manera sencilla de usar la protección de datos sin tener que
depender DI. El DataProtectionProvider escriba implementa IDataProtectionProvider. Construir
DataProtectionProvider sólo requiere proporcionar un DirectoryInfo instancia para indicar dónde se deben
almacenar las claves criptográficas del proveedor, tal como se muestra en el ejemplo de código siguiente:
using System;
using System.IO;
using Microsoft.AspNetCore.DataProtection;

public class Program


{
public static void Main(string[] args)
{
// Get the path to %LOCALAPPDATA%\myapp-keys
var destFolder = Path.Combine(
System.Environment.GetEnvironmentVariable("LOCALAPPDATA"),
"myapp-keys");

// Instantiate the data protection system at this folder


var dataProtectionProvider = DataProtectionProvider.Create(
new DirectoryInfo(destFolder));

var protector = dataProtectionProvider.CreateProtector("Program.No-DI");


Console.Write("Enter input: ");
var input = Console.ReadLine();

// Protect the payload


var protectedPayload = protector.Protect(input);
Console.WriteLine($"Protect returned: {protectedPayload}");

// Unprotect the payload


var unprotectedPayload = protector.Unprotect(protectedPayload);
Console.WriteLine($"Unprotect returned: {unprotectedPayload}");

Console.WriteLine();
Console.WriteLine("Press any key...");
Console.ReadKey();
}
}

/*
* SAMPLE OUTPUT
*
* Enter input: Hello world!
* Protect returned: CfDJ8FWbAn6...ch3hAPm1NJA
* Unprotect returned: Hello world!
*
* Press any key...
*/

De forma predeterminada, la DataProtectionProvider tipo concreto no cifra el material de clave sin procesar
antes la almacenarla en el sistema de archivos. Esto sirve para admitir escenarios donde los puntos de
desarrollador para un recurso compartido de red y el sistema de protección de datos no pueden deducir
automáticamente un mecanismo de cifrado de clave adecuado en rest.
Además, el DataProtectionProvider tipo concreto no aislar las aplicaciones de de forma predeterminada. Todas
las aplicaciones con el mismo directorio clave pueden compartir cargas siempre y cuando sus finalidad
parámetros coincide con.
El DataProtectionProvider constructor acepta una devolución de llamada de configuración opcionales que puede
usarse para ajustar los comportamientos del sistema. El ejemplo siguiente muestra el aislamiento de
restauración con una llamada explícita a SetApplicationName. El ejemplo también muestra la configuración del
sistema para cifrar automáticamente las claves permanentes mediante DPAPI de Windows. Si el directorio
apunta a un recurso compartido UNC, es recomendable que se va a distribuir un certificado compartido entre
todos los equipos correspondientes como configurar el sistema para usar el cifrado basada en certificados con
una llamada a ProtectKeysWithCertificate.
using System;
using System.IO;
using Microsoft.AspNetCore.DataProtection;

public class Program


{
public static void Main(string[] args)
{
// Get the path to %LOCALAPPDATA%\myapp-keys
var destFolder = Path.Combine(
System.Environment.GetEnvironmentVariable("LOCALAPPDATA"),
"myapp-keys");

// Instantiate the data protection system at this folder


var dataProtectionProvider = DataProtectionProvider.Create(
new DirectoryInfo(destFolder),
configuration =>
{
configuration.SetApplicationName("my app name");
configuration.ProtectKeysWithDpapi();
});

var protector = dataProtectionProvider.CreateProtector("Program.No-DI");


Console.Write("Enter input: ");
var input = Console.ReadLine();

// Protect the payload


var protectedPayload = protector.Protect(input);
Console.WriteLine($"Protect returned: {protectedPayload}");

// Unprotect the payload


var unprotectedPayload = protector.Unprotect(protectedPayload);
Console.WriteLine($"Unprotect returned: {unprotectedPayload}");

Console.WriteLine();
Console.WriteLine("Press any key...");
Console.ReadKey();
}
}

TIP
Instancias de la DataProtectionProvider tipo concreto son caros de crear. Si una aplicación mantiene varias instancias de
este tipo y si todo está usando el mismo directorio de almacenamiento de claves, puede degradar el rendimiento de la
aplicación. Si usas el DataProtectionProvider tipo, se recomienda que cree este tipo una vez y volver a usarlo tanto
como sea posible. El DataProtectionProvider tipo y todos IDataProtector instancias creadas a partir de los son seguras
para subprocesos para distintos llamadores.
API de extensibilidad de protección de datos de
ASP.NET Core
21/06/2018 • 2 minutes to read • Edit Online

Extensibilidad de criptografía de núcleo


Extensibilidad de administración de claves
Otras API
Extensibilidad de criptografía de núcleo de ASP.NET
Core
22/06/2018 • 11 minutes to read • Edit Online

WARNING
Los tipos que implementan cualquiera de las interfaces siguientes deben ser seguro para subprocesos para distintos
llamadores.

IAuthenticatedEncryptor
El IAuthenticatedEncryptor interfaz es el bloque de creación básico del subsistema criptográfico. Por lo
general, hay un IAuthenticatedEncryptor por clave y la instancia de IAuthenticatedEncryptor contiene todos los
material de clave de cifrado y algoritmo información necesaria para realizar operaciones criptográficas.
Como sugiere su nombre, el tipo es responsable de proporcionar servicios de cifrado y descifrado autenticados.
Expone las siguientes API de dos.
Descifrar (ArraySegment texto cifrado, ArraySegment additionalAuthenticatedData): byte]
Cifrar (ArraySegment texto simple, ArraySegment additionalAuthenticatedData): byte]
El método Encrypt devuelve un blob que incluye el texto sin formato descifra y una etiqueta de autenticación. La
etiqueta de autenticación debe incluir los datos adicionales autenticados (AAD ), aunque el AAD propio no
necesita poder recuperarse desde la carga final. El método Decrypt valida la etiqueta de autenticación y devuelve
la carga deciphered. Todos los errores (excepto ArgumentNullException y similar) deben homogeneizarse a
CryptographicException.

NOTE
La propia instancia IAuthenticatedEncryptor realmente no debe contener el material de clave. Por ejemplo, la
implementación puede delegar a un HSM para todas las operaciones.

Cómo crear un IAuthenticatedEncryptor


ASP.NET Core 2.x
ASP.NET Core 1.x
El IAuthenticatedEncryptorFactory interfaz representa un tipo que sabe cómo crear un
IAuthenticatedEncryptor instancia. La API es como sigue.
CreateEncryptorInstance (clave IKey): IAuthenticatedEncryptor
Para cualquier instancia de IKey determinada, los sistemas de cifrado autenticados creados por el método
CreateEncryptorInstance deben considerarse equivalentes, como en el ejemplo de código siguiente.
// we have an IAuthenticatedEncryptorFactory instance and an IKey instance
IAuthenticatedEncryptorFactory factory = ...;
IKey key = ...;

// get an encryptor instance and perform an authenticated encryption operation


ArraySegment<byte> plaintext = new ArraySegment<byte>(Encoding.UTF8.GetBytes("plaintext"));
ArraySegment<byte> aad = new ArraySegment<byte>(Encoding.UTF8.GetBytes("AAD"));
var encryptor1 = factory.CreateEncryptorInstance(key);
byte[] ciphertext = encryptor1.Encrypt(plaintext, aad);

// get another encryptor instance and perform an authenticated decryption operation


var encryptor2 = factory.CreateEncryptorInstance(key);
byte[] roundTripped = encryptor2.Decrypt(new ArraySegment<byte>(ciphertext), aad);

// the 'roundTripped' and 'plaintext' buffers should be equivalent

IAuthenticatedEncryptorDescriptor (ASP.NET Core solo 2.x)


ASP.NET Core 2.x
ASP.NET Core 1.x
El IAuthenticatedEncryptorDescriptor interfaz representa un tipo que sabe cómo exportar a XML. La API es
como sigue.
ExportToXml(): XmlSerializedDescriptorInfo

Serialización XML
La diferencia principal entre IAuthenticatedEncryptor y IAuthenticatedEncryptorDescriptor es que el descriptor
sabe cómo crear el sistema de cifrado y proporcionarle argumentos válidos. Tenga en cuenta un
IAuthenticatedEncryptor cuya implementación se basa en SymmetricAlgorithm y KeyedHashAlgorithm. Trabajo
del sistema de cifrado es consumen estos tipos, pero no conoce necesariamente estos tipos de proceden, por lo
que realmente no se puede escribir una descripción de cómo volver a sí mismo si se reinicia la aplicación
adecuada. El descriptor de actúa como un nivel más alto a partir de esto. Puesto que el descriptor sabe cómo
crear la instancia de sistema de cifrado (p. ej., sabe cómo crear los algoritmos necesarios), puede serializar esa
información en forma de XML para que la instancia de sistema de cifrado se puede volver a crear después de
restablece una aplicación.
El descriptor de se puede serializar a través de su rutina de ExportToXml. Esta rutina devuelve un
XmlSerializedDescriptorInfo que contiene dos propiedades: la representación de XElement de descriptor y el tipo
que representa un IAuthenticatedEncryptorDescriptorDeserializer que puede ser se usa para restablecerse este
descriptor dada la XElement correspondiente.
El descriptor serializado puede contener información confidencial como material de clave de cifrado. El sistema de
protección de datos tiene compatibilidad integrada para cifrar la información antes de que se conservan en el
almacenamiento. Para aprovechar estas características, el descriptor debería marcar el elemento que contiene
información confidencial con el nombre de atributo "requiresEncryption" (xmlns
"http://schemas.asp.net/2015/03/dataProtection"), valor "true".

TIP
Hay una API auxiliar para establecer este atributo. Llame al método de extensión que XElement.markasrequiresencryption()
ubicado en el espacio de nombres Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.

También puede haber casos donde el descriptor serializado no contiene información confidencial. Considere la
posibilidad de nuevo el caso de una clave criptográfica que se almacenan en un HSM. El material de clave no se
puede escribir el descriptor al serializar a sí mismo porque el HSM no expone el material en formato de texto
simple. En su lugar, puede escribir el descriptor de la versión de clave ajusta de la clave (si el HSM permite la
exportación de este modo) o el identificador único del HSM para la clave.

IAuthenticatedEncryptorDescriptorDeserializer
El IAuthenticatedEncryptorDescriptorDeserializer interfaz representa un tipo que sabe cómo deserializar
una instancia de IAuthenticatedEncryptorDescriptor desde un XElement. Expone un único método:
ImportFromXml (elemento de XElement): IAuthenticatedEncryptorDescriptor
El método ImportFromXml toma el XElement que devolvió IAuthenticatedEncryptorDescriptor.ExportToXml y
crea un equivalente de la IAuthenticatedEncryptorDescriptor original.
Tipos que implementan IAuthenticatedEncryptorDescriptorDeserializer deben tener uno de los dos constructores
públicos siguientes:
.ctor(IServiceProvider)
.ctor()

NOTE
IServiceProvider pasado al constructor puede ser null.

El generador de nivel superior


ASP.NET Core 2.x
ASP.NET Core 1.x
El AlgorithmConfiguration clase representa un tipo que sabe cómo crear IAuthenticatedEncryptorDescriptor
instancias. Expone una sola API.
CreateNewDescriptor(): IAuthenticatedEncryptorDescriptor
Considerar AlgorithmConfiguration como el generador de nivel superior. La configuración actúa como una
plantilla. Encapsula información algorítmica (p. ej., esta configuración produce descriptores con una clave maestra
de AES -128-GCM ), pero aún no está asociada a una clave específica.
Cuando se llama a CreateNewDescriptor, material de clave nueva se crea únicamente para esta llamada y se
genera un nuevo IAuthenticatedEncryptorDescriptor que ajusta este material de clave y la información
algorítmica necesarios para consumir el material. El material de clave podría creó en software (y se mantienen en
la memoria), podría ser crea y mantiene dentro de un HSM y así sucesivamente. El punto fundamental es que las
dos llamadas a CreateNewDescriptor nunca deben crearse instancias de IAuthenticatedEncryptorDescriptor
equivalente.
El tipo de AlgorithmConfiguration actúa como punto de entrada para las rutinas de creación de claves como
reversión de clave automática. Para cambiar la implementación de todas las claves futuras, establezca la
propiedad AuthenticatedEncryptorConfiguration en KeyManagementOptions.
Extensibilidad de administración de claves en
ASP.NET Core
22/06/2018 • 15 minutes to read • Edit Online

TIP
Leer la administración de claves sección antes de leer esta sección, tal y como se explican algunos de los conceptos
fundamentales de estas API.

WARNING
Los tipos que implementan cualquiera de las interfaces siguientes deben ser seguro para subprocesos para distintos
llamadores.

Key
La IKey interfaz es la representación básica de una clave en el sistema de cifrado. La clave de término se utiliza
aquí en el sentido abstracto, no en el sentido de "material clave criptográfico" literal. Una clave tiene las siguientes
propiedades:
Fechas de expiración, la creación y la activación
Estado de revocación
Identificador de clave (GUID )
ASP.NET Core 2.x
ASP.NET Core 1.x
Además, IKey expone un CreateEncryptor método que se puede usar para crear un IAuthenticatedEncryptor
instancia asociado a esta clave.

NOTE
No hay ninguna API para recuperar el material criptográfico sin formato de un IKey instancia.

IKeyManager
El IKeyManager interfaz representa un objeto responsable de almacenamiento de claves general, la recuperación y
la manipulación. Expone tres operaciones de alto nivel:
Cree una nueva clave y almacenar los datos en almacenamiento.
Obtener todas las claves de almacenamiento.
Revocar una o varias claves y conservar la información de revocación en el almacenamiento.
WARNING
Escribir una IKeyManager una tarea muy avanzada y la mayoría de los desarrolladores no debe intentarlo. En su lugar, la
mayoría de los desarrolladores deben aprovechar las ventajas de las funciones que ofrece el XmlKeyManager clase.

XmlKeyManager
El XmlKeyManager tipo es la implementación concreta en el cuadro de IKeyManager . Proporciona varias funciones
útiles, incluidas la custodia de clave y el cifrado de claves en reposo. Las claves en este sistema se representan
como elementos XML (en concreto, XElement).
XmlKeyManager depende de otros componentes en el curso de cumplir sus tareas:
ASP.NET Core 2.x
ASP.NET Core 1.x
AlgorithmConfiguration , que determina los algoritmos utilizados por las nuevas claves.
IXmlRepository , que controla donde las claves se conservan en almacenamiento.
IXmlEncryptor [opcional], que permite cifrar las claves en reposo.
IKeyEscrowSink [opcional], que proporciona servicios de custodia de clave.

A continuación se muestran los diagramas de alto nivel que indican cómo se conectan juntos estos componentes
en XmlKeyManager .
ASP.NET Core 2.x
ASP.NET Core 1.x

Creación de clave / CreateNewKey


En la implementación de CreateNewKey , AlgorithmConfiguration componente se utiliza para crear un nombre
único IAuthenticatedEncryptorDescriptor , que, a continuación, se serializa como XML. Si un receptor de custodia
de clave está presente, el XML sin formato (sin cifrar) se proporciona al receptor de almacenamiento a largo plazo.
A continuación, se ejecuta el XML sin cifrar a través de un IXmlEncryptor (si es necesario) para generar el
documento XML cifrado. Este documento cifrada se almacena en almacenamiento a largo plazo a través de la
IXmlRepository . ( Si no hay ningún IXmlEncryptor está configurado, se guarda el documento sin cifrar en la
IXmlRepository .)

ASP.NET Core 2.x


ASP.NET Core 1.x
Recuperación de clave / GetAllKeys
En la implementación de GetAllKeys , el XML documenta las claves que representan y se leen las revocaciones de
subyacente IXmlRepository . Si estos documentos están cifrados, el sistema les descifrará automáticamente.
XmlKeyManager crea la correspondiente IAuthenticatedEncryptorDescriptorDeserializer instancias para deserializar
los documentos de nuevo en IAuthenticatedEncryptorDescriptor instancias, que, a continuación, se incluyen en
persona IKey instancias. Esta colección de IKey instancias se devuelve al llamador.
Encontrará más información sobre los elementos XML determinados en el documento de formato de
almacenamiento de claves.

IXmlRepository
El IXmlRepository interfaz representa un tipo que pueda persista código XML en y recuperar el XML de un
almacén de copia de seguridad. Expone dos API:
GetAllElements(): IReadOnlyCollection
StoreElement (elemento XElement, cadena friendlyName)
Las implementaciones de IXmlRepository no es necesario analizar el XML que se pasan a través de ellos. Debe
tratar los documentos XML como opaco y permitir que los niveles superiores a preocuparse sobre cómo generar
y analizar los documentos.
Hay dos tipos integrados concretos que implementan IXmlRepository : FileSystemXmlRepository y
RegistryXmlRepository . Consulte la documento de proveedores de almacenamiento de claves para obtener más
información. Registrar un personalizado IXmlRepository sería la manera adecuada para usar un almacén de
respaldo diferentes, por ejemplo, el almacenamiento de blobs de Azure.
Para cambiar el repositorio predeterminado de toda la aplicación, registrar un personalizado IXmlRepository
instancia:
ASP.NET Core 2.x
ASP.NET Core 1.x

services.Configure<KeyManagementOptions>(options => options.XmlRepository = new MyCustomXmlRepository());

IXmlEncryptor
El IXmlEncryptor interfaz representa un tipo que puede cifrar un elemento XML de texto simple. Expone una
única API:
Cifrar (plaintextElement de XElement): EncryptedXmlInfo
Si un número de serie IAuthenticatedEncryptorDescriptor contiene elementos marcados como "requiere cifrado",
a continuación, XmlKeyManager ejecutará esos elementos a través de la configurada IXmlEncryptor del Encrypt
método y se conservará el elemento descifra en lugar de la elemento de texto simple para el IXmlRepository . El
resultado de la Encrypt método es un EncryptedXmlInfo objeto. Este objeto es un contenedor que contiene tanto
el resultante descifra XElement y el tipo que representa un IXmlDecryptor que puede utilizarse para descifrar el
elemento correspondiente.
Hay cuatro tipos integrados concretos que implementan IXmlEncryptor :
CertificateXmlEncryptor
DpapiNGXmlEncryptor
DpapiXmlEncryptor
NullXmlEncryptor

Consulte la cifrado de clave en el documento de rest para obtener más información.


Para cambiar el mecanismo de cifrado de clave en el resto de predeterminado de toda la aplicación, registrar un
personalizado IXmlEncryptor instancia:
ASP.NET Core 2.x
ASP.NET Core 1.x

services.Configure<KeyManagementOptions>(options => options.XmlEncryptor = new MyCustomXmlEncryptor());

IXmlDecryptor
El interfaz representa un tipo que sabe cómo descifrar un
IXmlDecryptor XElement que se descifra mediante una
IXmlEncryptor . Expone una única API:

Descifrar (encryptedElement de XElement): XElement


El método deshace el cifrado realizado por IXmlEncryptor.Encrypt . Por lo general, cada hormigón
Decrypt
IXmlEncryptor implementación tendrá un hormigón correspondiente IXmlDecryptor implementación.

Los tipos que implementan IXmlDecryptor debe tener uno de los dos constructores públicos siguientes:
.ctor(IServiceProvider)
.ctor()

NOTE
El IServiceProvider pasado al constructor puede ser null.

IKeyEscrowSink
El IKeyEscrowSink interfaz representa un tipo que puede realizar la custodia de la información confidencial.
Recuerde que descriptores serializados podrían contener información confidencial (por ejemplo, el material
criptográfico) y esto es lo que ha provocado la introducción de la IXmlEncryptor escriba en primer lugar. Sin
embargo, los accidentes y llaveros pueden eliminarse o están dañados.
La interfaz de custodia proporciona una trama de escape de emergencia, permitir el acceso al XML serializado sin
procesar antes de que se transforme en ninguno configurado IXmlEncryptor. La interfaz expone una única API:
Almacén (keyId de Guid, elemento de XElement)
Depende del IKeyEscrowSink implementación para controlar el elemento proporcionado de forma segura
coherente con la directiva empresarial. Una posible implementación podría ser para que el receptor de custodia
cifrar el elemento XML mediante un certificado X.509 corporativo conocido donde se ha custodiado clave privada
del certificado; el CertificateXmlEncryptor tipo puede ayudarle con esto. El IKeyEscrowSink implementación
también es responsable de conservar el elemento proporcionado de forma adecuada.
De forma predeterminada ningún mecanismo de custodia está habilitado, aunque los administradores de servidor
pueden configurarlo global. También puede configurar mediante programación a través de la
IDataProtectionBuilder.AddKeyEscrowSink método tal como se muestra en el ejemplo siguiente. El
AddKeyEscrowSink reflejado de las sobrecargas de método la IServiceCollection.AddSingleton y
IServiceCollection.AddInstance sobrecargas, como IKeyEscrowSink instancias están pensadas para ser singletons.
Si hay varios IKeyEscrowSink instancias registradas, se llamará a cada uno de ellos durante la generación de
claves, por lo que las claves se pueden custodiar a varios mecanismos simultáneamente.
No hay ninguna API para leer el material de un IKeyEscrowSink instancia. Esto es coherente con la teoría del
diseño del mecanismo de custodia: se ha diseñado para hacer que el material de clave sea accesible a una
autoridad de confianza y, puesto que la aplicación de sí mismo no es una entidad de confianza, no debería tener
acceso a su propio material custodiada.
El código de ejemplo siguiente muestra cómo crear y registrar un IKeyEscrowSink donde se custodiar claves de
modo que solo los miembros del "CONTOSODomain Admins" pueden recuperarlos.

NOTE
Para ejecutar este ejemplo, debe ser en un equipo con Windows 8 Unidos a un dominio / máquina con Windows Server
2012 y el controlador de dominio deben ser Windows Server 2012 o posterior.

using System;
using System.IO;
using System.Xml.Linq;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.DataProtection.KeyManagement;
using Microsoft.AspNetCore.DataProtection.XmlEncryption;
using Microsoft.Extensions.DependencyInjection;

public class Program


{
public static void Main(string[] args)
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(@"c:\temp-keys"))
.ProtectKeysWithDpapi()
.AddKeyEscrowSink(sp => new MyKeyEscrowSink(sp));
var services = serviceCollection.BuildServiceProvider();

// get a reference to the key manager and force a new key to be generated
Console.WriteLine("Generating new key...");
var keyManager = services.GetService<IKeyManager>();
keyManager.CreateNewKey(
activationDate: DateTimeOffset.Now,
expirationDate: DateTimeOffset.Now.AddDays(7));
}

// A key escrow sink where keys are escrowed such that they
// can be read by members of the CONTOSO\Domain Admins group.
private class MyKeyEscrowSink : IKeyEscrowSink
private class MyKeyEscrowSink : IKeyEscrowSink
{
private readonly IXmlEncryptor _escrowEncryptor;

public MyKeyEscrowSink(IServiceProvider services)


{
// Assuming I'm on a machine that's a member of the CONTOSO
// domain, I can use the Domain Admins SID to generate an
// encrypted payload that only they can read. Sample SID from
// https://technet.microsoft.com/library/cc778824(v=ws.10).aspx.
_escrowEncryptor = new DpapiNGXmlEncryptor(
"SID=S-1-5-21-1004336348-1177238915-682003330-512",
DpapiNGProtectionDescriptorFlags.None,
services);
}

public void Store(Guid keyId, XElement element)


{
// Encrypt the key element to the escrow encryptor.
var encryptedXmlInfo = _escrowEncryptor.Encrypt(element);

// A real implementation would save the escrowed key to a


// write-only file share or some other stable storage, but
// in this sample we'll just write it out to the console.
Console.WriteLine($"Escrowing key {keyId}");
Console.WriteLine(encryptedXmlInfo.EncryptedElement);

// Note: We cannot read the escrowed key material ourselves.


// We need to get a member of CONTOSO\Domain Admins to read
// it for us in the event we need to recover it.
}
}
}

/*
* SAMPLE OUTPUT
*
* Generating new key...
* Escrowing key 38e74534-c1b8-4b43-aea1-79e856a822e5
* <encryptedKey>
* <!-- This key is encrypted with Windows DPAPI-NG. -->
* <!-- Rule: SID=S-1-5-21-1004336348-1177238915-682003330-512 -->
* <value>MIIIfAYJKoZIhvcNAQcDoIIIbTCCCGkCAQ...T5rA4g==</value>
* </encryptedKey>
*/
API de protección de datos de varios núcleos de
ASP.NET
22/06/2018 • 2 minutes to read • Edit Online

WARNING
Los tipos que implementan cualquiera de las interfaces siguientes deben ser seguro para subprocesos para distintos
llamadores.

ISecret
El ISecret interfaz representa un valor secreto, como material de clave de cifrado. Contiene la superficie de API
siguiente:
Length : int

Dispose() : void

WriteSecretIntoBuffer(ArraySegment<byte> buffer) : void

El WriteSecretIntoBuffer método rellena el búfer proporcionado con el valor sin formato del secreto. El motivo de
esta API toma el búfer como un parámetro en lugar de devolver un byte[] directamente es esto da al llamador la
oportunidad para anclar el objeto de búfer, limitar la exposición de secreto para el recolector de elementos no
utilizados administrado.
El Secret tipo es una implementación concreta de ISecret donde el valor secreto se almacena en memoria en el
proceso. En plataformas de Windows, el valor secreto se cifra mediante CryptProtectMemory.
Implementación de protección de datos de ASP.NET
Core
21/06/2018 • 2 minutes to read • Edit Online

Detalles de cifrado autenticado


Derivación de subclave y cifrado autenticado
Encabezados de contexto
Administración de claves
Proveedores de almacenamiento de claves
Cifrado de claves en reposo
Inmutabilidad de claves y configuración
Formato de almacenamiento de claves
Proveedores de protección de datos efímeros
Detalles de cifrado autenticado en ASP.NET Core
22/06/2018 • 4 minutes to read • Edit Online

Llamadas a IDataProtector.Protect son operaciones de cifrado autenticado. El método Protect ofrece


confidencialidad y la autenticidad y está asociado a la cadena de propósito que se usó para esta instancia concreta
de IDataProtector se deriva su raíz IDataProtectionProvider.
IDataProtector.Protect toma un parámetro de texto simple de byte [] y genera una byte [] protegido carga, cuyo
formato se describe a continuación. (También hay una sobrecarga del método de extensión que toma un
parámetro de cadena de texto simple y devuelve una carga protegido de cadena. Si se usa esta API seguirá
teniendo el formato de carga protegido el por debajo de la estructura, pero será codificado en base64url.)

Formato de carga protegido


El formato de carga protegido consta de tres componentes principales:
Encabezado mágico de 32 bits que identifica la versión del sistema de protección de datos.
Identificador de clave de 128 bits que identifica la clave utilizada para proteger esta carga determinada.
El resto de la carga protegido es específico para el sistema de cifrado encapsulada por esta clave. En el
ejemplo siguiente representa la clave de un cifrado AES -256-CBC + HMACSHA256 cifrado y la carga se
subdivide como sigue: * el modificador de tecla A 128 bits. * Un vector de inicialización de 128 bits. * 48
bytes de salida de AES -256-CBC. * Una etiqueta de autenticación HMACSHA256.
Una carga protegido de ejemplo se ilustra a continuación.

09 F0 C9 F0 80 9C 81 0C 19 66 19 40 95 36 53 F8
AA FF EE 57 57 2F 40 4C 3F 7F CC 9D CC D9 32 3E
84 17 99 16 EC BA 1F 4A A1 18 45 1F 2D 13 7A 28
79 6B 86 9C F8 B7 84 F9 26 31 FC B1 86 0A F1 56
61 CF 14 58 D3 51 6F CF 36 50 85 82 08 2D 3F 73
5F B0 AD 9E 1A B2 AE 13 57 90 C8 F5 7C 95 4E 6A
8A AA 06 EF 43 CA 19 62 84 7C 11 B2 C8 71 9D AA
52 19 2E 5B 4C 1E 54 F0 55 BE 88 92 12 C1 4B 5E
52 C9 74 A0

Desde el formato de carga por encima de los primeros 32 bits o 4 bytes son el encabezado mágico identifica la
versión (09 F0 C9 F0)
Los siguientes 128 bits o 16 bytes es el identificador de clave (80 9 81 C 0c 19 66 19 40 95 36 53 F8 AA FF EE
57)
El resto contiene la carga y es específico para el formato utilizado.

WARNING
Todas las cargas protegidas para una clave determinada se iniciará con el mismo encabezado de 20 bytes (valor mágica, Id.
de clave). Los administradores pueden usar este hecho con fines de diagnóstico para la aproximación cuando se genera una
carga. Por ejemplo, la carga anterior corresponde a la clave {0c819c80-6619-4019-9536-53f8aaffee57}. Si después de
comprobar el repositorio clave encuentra que la fecha de activación de esta clave específica fue 2015-01-01 y su fecha de
expiración era 2015-03-01, entonces es razonable suponer la carga (si no ha sido manipulado con) se ha generado dentro
de esa ventana, conceda a o tomar una pequeña factor de aglutinante a cada lado.
Subclave derivación y cifrado autenticado en
ASP.NET Core
22/06/2018 • 8 minutes to read • Edit Online

La mayoría de las teclas en el anillo de clave contiene alguna forma de entropía y tendrá algorítmica información
que indica "cifrado de modo CBC + validación HMAC" o "cifrado de GCM + validación". En estos casos, nos
referimos a la entropía incrustada como el material de creación de claves maestras (o KM ) para esta clave y
llevamos a cabo una función de derivación de claves para derivar las claves que se usará para las operaciones
criptográficas reales.

NOTE
Las claves son abstractas, y una implementación personalizada posible que no funcionen como sigue. Si la clave proporciona
su propia implementación de IAuthenticatedEncryptor en lugar de usar una de nuestras fábricas integradas, el
mecanismo se describe en esta sección ya no es aplicable.

Datos autenticados adicionales y subclave derivación


El IAuthenticatedEncryptor interfaz actúa como la interfaz básica para todas las operaciones de cifrado
autenticado. Su Encrypt método toma dos búferes: texto sin formato y additionalAuthenticatedData (AAD ). El
flujo de contenido de texto simple sin modificar la llamada a IDataProtector.Protect , pero el AAD generada por el
sistema y consta de tres componentes:
1. El encabezado de mágico de 32 bits 09 F0 C9 F0 que identifica esta versión del sistema de protección de
datos.
2. El identificador de clave de 128 bits.
3. Una cadena de longitud variable formado a partir de la cadena de fin que creó el IDataProtector que está
realizando esta operación.
Dado que el AAD es única para la tupla de los tres componentes, podemos usar se pueden para derivar nuevas
claves KM en lugar de usar KM propio en todos nuestros operaciones de cifrado. Para todas las llamadas a
IAuthenticatedEncryptor.Encrypt , realiza el proceso de derivación de claves siguiente:

(K_E, K_H) = SP800_108_CTR_HMACSHA512 (contextHeader K_M, AAD, || keyModifier)


En este caso, estamos llamando a KDF SP800-108 NIST en modo de contador (vea NIST SP800-108, s. 5.1) con
los siguientes parámetros:
Clave de derivación de claves (KDK) = K_M
PRF = HMACSHA512
etiqueta = additionalAuthenticatedData
contexto = contextHeader || keyModifier
El encabezado de contexto es de longitud variable y actúa esencialmente como una huella digital de los algoritmos
para el que nos estamos derivación K_E y K_H. El modificador de clave es una cadena de 128 bits que se genera
de forma aleatoria para cada llamada a Encrypt y sirve para asegurarse de con una sobrecarga de probabilidad
que KE y KH son únicos para esta operación de cifrado de autenticación específico, incluso si todos los demás
entrada KDF es constante.
Para el cifrado del modo CBC + las operaciones de validación de HMAC, | K_E | es la longitud de la clave de
cifrado de bloques simétrico y | K_H | es el tamaño de resumen de la rutina HMAC. Para el cifrado de GCM + las
operaciones de validación, | K_H | = 0.

Cifrado del modo CBC + validación HMAC


Una vez K_E se genera mediante el mecanismo anterior, se genera un vector de inicialización aleatorio y ejecutar el
algoritmo de cifrado de bloques simétrico para cifrar el texto sin formato. El vector de inicialización y el texto
cifrado, a continuación, se ejecutan a través de la rutina HMAC que se inicializa con la clave K_H para generar el
equipo Mac. Este proceso y el valor devuelto se representa gráficamente a continuación.

output:= keyModifier || iv || E_cbc (K_E,iv,data ) || HMAC (K_H, iv || E_cbc (K_E,iv,data ))

NOTE
El IDataProtector.Protect implementación le anteponer el encabezado mágica y el Id. de clave a salida antes de
devolverlo al llamador. Dado que el encabezado mágica y el Id. de clave son implícitamente forma parte de AAD, y dado que
el modificador de tecla se introduce como entrada a KDF, esto significa que cada byte único de la última carga devuelta es
autenticado por el equipo Mac.

El cifrado del modo de Galois/contador + validación


Una vez K_E se genera mediante el mecanismo anterior, se genera un valor aleatorio de 96 bits nonce y ejecutar el
algoritmo de cifrado de bloques simétrico para cifrar el texto sin formato y generar la etiqueta de autenticación de
128 bits.
salida: = keyModifier || nonce || E_gcm (K_E, nonce, de datos) || authTag

NOTE
Aunque GCM forma nativa es compatible con el concepto de AAD, nos estamos todavía alimentación AAD solo KDF original,
para pasar una cadena vacía a GCM para su parámetro AAD. La razón para esto es dos vertientes. En primer lugar, para
admitir la agilidad nunca queremos usar K_M directamente como la clave de cifrado. Además, GCM impone requisitos de
unicidad muy estrictos en sus entradas. La probabilidad de que la rutina de cifrado de GCM alguna vez invocado con dos o
más distintos conjuntos de datos de entrada con el mismo (clave, nonce) par no debe superar los 2 ^ 32. Si se soluciona K_E
no podemos realizar más de 2 ^ 32 operaciones de cifrado antes de que se ejecute mantiene del 2 ^ limitar -32. Esto puede
parecer un gran número de operaciones, pero un servidor web de tráfico elevado puede ir a través de solicitudes de 4 mil
millones en días simples, bien dentro de la duración normal de estas claves. A estar al día de 2 ^ límite de probabilidad-32,
seguimos utilizar un modificador de clave de 128 bits y 96 bits nonce, que extiende radicalmente el número de operaciones
puede usar para cualquier K_M determinado. Para simplificar el trabajo de diseño compartimos la ruta de acceso del código
KDF entre las operaciones de cifrado CBC y GCM y, puesto que ya se considera AAD en KDF no es necesario que se reenvíe
a la rutina GCM.
Encabezados de contexto en ASP.NET Core
22/06/2018 • 17 minutes to read • Edit Online

Segundo plano y la teoría


En el sistema de protección de datos, una "clave" significa autenticado de un objeto que puede proporcionar
servicios de cifrado. Cada clave está identificada por un identificador único (GUID ) y lleva con él algorítmica
información y al material entropic. Se pretende que cada clave llevar entropía único, pero el sistema no puede
exigir y también es necesario tener en cuenta para los desarrolladores que cambiaría el anillo de clave
manualmente mediante la modificación de la información de una clave existente en el anillo de clave algorítmica.
Para lograr los requisitos de seguridad tiene estos casos, el sistema de protección de datos tiene un concepto de
agilidad criptográfica, que permite de forma segura mediante un único valor entropic entre varios algoritmos
criptográficos.
Mayoría de los sistemas que son compatibles con agilidad criptográfica hacerlo mediante la inclusión de cierta
información de identificación sobre el algoritmo en la carga. OID del algoritmo suele ser un buen candidato para
esto. Sin embargo, un problema que encontramos es que hay varias maneras de especificar el mismo algoritmo:
"AES" (CNG ) y los administrados Aes, AesManaged, AesCryptoServiceProvider, AesCng y RijndaelManaged
(determinados parámetros específicos) clases todo realmente son las mismas lo y se tendría que mantener una
asignación de todos estos para el OID correcto. Si un desarrollador desea proporcionar un algoritmo
personalizado (o incluso otra implementación de AES ), tendría que Díganos su OID. Este paso de registro
adicional, la configuración de sistema es especialmente muy complicada.
Ejecución paso a paso atrás, decidimos que estábamos se está aproximando al problema de la dirección
equivocada. Un OID indica cuál es el algoritmo, pero se no realmente le interesa esto. Si se necesita usar un único
valor entropic de forma segura en los dos algoritmos diferentes, no es necesario para que podamos saber cuáles
son en realidad los algoritmos. ¿Qué nos realmente importa es su comportamiento. Cualquier algoritmo de
cifrado de bloques simétrico decente también es una permutación pseudoaleatoria segura (PRP ): corrija las
entradas (clave, el encadenamiento de texto simple de modo, IV ) y la salida de texto cifrado con una sobrecarga
probabilidad será distinta de cualquier otro cifrado por bloques simétrico algoritmo dada las entradas de la
mismas. Del mismo modo, cualquier función de hash con clave decente también es una función pseudoaleatoria
segura (PRF ), y debido a un conjunto de entrada fijo su salida muy será distinta de cualquier otra función de hash
con clave.
Este concepto de PRPs y PRFs seguros se usa para crear un encabezado de contexto. Este encabezado de contexto
actúa esencialmente como una huella digital estable sobre los algoritmos en uso para una operación determinada,
así como la agilidad criptográfica necesaria para el sistema de protección de datos. Este encabezado es
"reproducible" y se utiliza posteriormente como parte de la proceso de derivación de la subclave. Hay dos maneras
diferentes para generar el encabezado de contexto de función de los modos de funcionamiento de los algoritmos
subyacentes.

Cifrado del modo CBC + autenticación HMAC


El encabezado de contexto está formada por los siguientes componentes:
[16 bits] El valor 00 00, que es un marcador de lo que significa "cifrado CBC + autenticación HMAC".
[32 bits] La longitud de clave (en bytes, big-endian) del algoritmo de cifrado de bloques simétrico.
[32 bits] El tamaño de bloque (en bytes, big-endian) del algoritmo de cifrado de bloques simétrico.
[32 bits] La longitud de clave (en bytes, big-endian) del algoritmo HMAC. (Actualmente el tamaño de clave
siempre coincide con el tamaño de texto implícita.)
[32 bits] El tamaño de texto implícita (en bytes, big-endian) del algoritmo HMAC.
EncCBC (K_E, IV, ""), que es el resultado del algoritmo de cifrado de bloques simétrico dado una entrada de
cadena vacía y donde IV es un vector de ceros. La construcción de K_E se describe a continuación.
MAC (K_H, ""), que es el resultado del algoritmo HMAC dado una entrada de cadena vacía. La construcción
de K_H se describe a continuación.
Lo ideal es que, podríamos pasamos vectores de ceros para K_E y K_H. Sin embargo, debe evitar la situación
donde el algoritmo subyacente comprueba la existencia de claves débiles antes de realizar cualquier operación
(especialmente DES y 3DES ), lo que impide utilizar un modelo simple o repeatable como un vector de ceros.
En su lugar, usamos NIST SP800-108 KDF en modo de contador (vea NIST SP800-108, s. 5.1) con una clave de
longitud cero, etiqueta y contexto y HMACSHA512 como el PRF subyacente. Se derivan | K_E | + | K_H | bytes de
salida, a continuación, descomponer el resultado en K_E y K_H por sí mismos. Matemáticamente, se representa
como se indica a continuación.
(K_E || K_H) = SP800_108_CTR (prf = HMACSHA512, key = "", etiqueta = "", contexto = "")
Ejemplo: AES -192-CBC + HMACSHA256
Por ejemplo, considere el caso donde el algoritmo de cifrado de bloques simétrico es AES -192-CBC y el algoritmo
de validación es HMACSHA256. El sistema generaría el encabezado de contexto mediante los pasos siguientes.
En primer lugar, se permiten (K_E || K_H) = SP800_108_CTR (prf = HMACSHA512, key = "", etiqueta = "",
contexto = ""), donde | K_E | = 192 bits y | K_H | = 256 bits por los algoritmos especificados. Esto conduce al K_E =
5BB6... 21DD y K_H = A04A... 00A9 en el ejemplo siguiente:

5B B6 C9 83 13 78 22 1D 8E 10 73 CA CF 65 8E B0
61 62 42 71 CB 83 21 DD A0 4A 05 00 5B AB C0 A2
49 6F A5 61 E3 E2 49 87 AA 63 55 CD 74 0A DA C4
B7 92 3D BF 59 90 00 A9

A continuación, calcular Enc_CBC (K_E, IV, "") de AES -192-CBC dado IV = 0 * y K_E como anteriormente.
resultado: = F474B1872B3B53E4721DE19C0841DB6F
A continuación, calcular MAC (K_H, "") para HMACSHA256 dado K_H como anteriormente.
resultado: = D4791184B996092EE1202F36E8608FA8FBD98ABDFF5402F264B1D7211536220C
Esto produce el encabezado de contexto completo siguiente:

00 00 00 00 00 18 00 00 00 10 00 00 00 20 00 00
00 20 F4 74 B1 87 2B 3B 53 E4 72 1D E1 9C 08 41
DB 6F D4 79 11 84 B9 96 09 2E E1 20 2F 36 E8 60
8F A8 FB D9 8A BD FF 54 02 F2 64 B1 D7 21 15 36
22 0C

Este encabezado de contexto es la huella digital del par de algoritmo de cifrado autenticado (cifrado de AES -192-
CBC + HMACSHA256 validación). Los componentes, como se describe anteriormente son:
el marcador (00 00)
la longitud de clave de cifrado de bloque (00 00 00 18)
el tamaño de bloque de cifrado de bloque (00 00 00 10)
la longitud de clave de HMAC (00 00 00 20)
el tamaño de la síntesis HMAC (00 00 00 20)
el cifrado por bloques salida PRP (F4 74 - DB 6F ) y
la salida de HMAC PRF (D4 79 - final).

NOTE
El cifrado del modo CBC + HMAC encabezado de contexto de autenticación se basa en la misma forma, independientemente
de si se proporcionan las implementaciones de algoritmos CNG de Windows o tipos administrados SymmetricAlgorithm y
KeyedHashAlgorithm. Esto permite que aplicaciones que se ejecutan en sistemas operativos diferentes generar de forma
confiable el mismo encabezado de contexto, aunque las implementaciones de los algoritmos difieren entre sistemas
operativos. (En la práctica, la KeyedHashAlgorithm no tiene que ser un HMAC correcto. Puede ser cualquier tipo de
algoritmo hash con clave.)

Ejemplo: 3DES -192-CBC + HMACSHA1


En primer lugar, se permiten (K_E || K_H) = SP800_108_CTR (prf = HMACSHA512, key = "", etiqueta = "",
contexto = ""), donde | K_E | = 192 bits y | K_H | = 160 bits por los algoritmos especificados. Esto conduce al K_E =
A219... E2BB y K_H = DC4A... B464 en el ejemplo siguiente:

A2 19 60 2F 83 A9 13 EA B0 61 3A 39 B8 A6 7E 22
61 D9 F8 6C 10 51 E2 BB DC 4A 00 D7 03 A2 48 3E
D1 F7 5A 34 EB 28 3E D7 D4 67 B4 64

A continuación, calcular Enc_CBC (K_E, IV, "") para 3DES -192-CBC dado IV = 0 * y K_E como anteriormente.
resultado: = ABB100F81E53E10E
A continuación, calcular MAC (K_H, "") para HMACSHA1 dado K_H como anteriormente.
resultado: = 76EB189B35CF03461DDF877CD9F4B1B4D63A7555
Esto genera el encabezado de contexto completo que es una huella digital de los autenticados par de algoritmo de
cifrado (cifrado 3DES -192-CBC + validación HMACSHA1), se muestra a continuación:

00 00 00 00 00 18 00 00 00 08 00 00 00 14 00 00
00 14 AB B1 00 F8 1E 53 E1 0E 76 EB 18 9B 35 CF
03 46 1D DF 87 7C D9 F4 B1 B4 D6 3A 75 55

Los componentes se dividen como sigue:


el marcador (00 00)
la longitud de clave de cifrado de bloque (00 00 00 18)
el tamaño de bloque de cifrado de bloque (00 00 00 08)
la longitud de clave de HMAC (00 00 00 14)
el tamaño de la síntesis HMAC (00 00 00 14)
el cifrado por bloques salida PRP (B1 AB - E1 0E ) y
la salida de HMAC PRF (76 EB - final).

El cifrado del modo de Galois/contador + autenticación


El encabezado de contexto está formada por los siguientes componentes:
[16 bits] El valor 00 01, que es un marcador de lo que significa "cifrado de GCM + autenticación".
[32 bits] La longitud de clave (en bytes, big-endian) del algoritmo de cifrado de bloques simétrico.
[32 bits] El tamaño (en bytes, big-endian) nonce que usa durante las operaciones de cifrado autenticado.
(En nuestro sistema, esto se fija en tamaño nonce = 96 bits.)
[32 bits] El tamaño de bloque (en bytes, big-endian) del algoritmo de cifrado de bloques simétrico. (Para
GCM, esto se fija en el tamaño de bloque = 128 bits.)
[32 bits] La autenticación etiqueta tamaño (en bytes, big-endian) creado por la función de cifrado
autenticado. (En nuestro sistema, esto se fija en el tamaño de la etiqueta = 128 bits.)
[128 bits] La etiqueta de Enc_GCM (K_E, nonce, ""), que es el resultado del algoritmo de cifrado de bloques
simétrico dado una entrada de cadena vacía y donde nonce es un vector de ceros de 96 bits.
K_E se deduce usando el mismo mecanismo como en el cifrado CBC + el escenario de autenticación de HMAC.
Sin embargo, puesto que no hay ninguna K_H en play aquí, se suelen tener | K_H | = 0, y el algoritmo se contrae
en el siguiente formulario.
K_E = SP800_108_CTR (prf = HMACSHA512, key = "", etiqueta = "", contexto = "")
Ejemplo: AES -256-GCM
En primer lugar, permiten K_E = SP800_108_CTR (prf = HMACSHA512, key = "", etiqueta = "", contexto = ""),
donde | K_E | = 256 bits.
K_E := 22BC6F1B171C08C4AE2F27444AF8FC8B3087A90006CAEA91FDCFB47C1B8733B8
A continuación, calcular la etiqueta de autenticación de Enc_GCM (K_E, nonce, "") de AES -256-GCM dado nonce
= 096 y K_E como anteriormente.
resultado: = E7DCCE66DF855A323A6BB7BD7A59BE45
Esto produce el encabezado de contexto completo siguiente:

00 01 00 00 00 20 00 00 00 0C 00 00 00 10 00 00
00 10 E7 DC CE 66 DF 85 5A 32 3A 6B B7 BD 7A 59
BE 45

Los componentes se dividen como sigue:


el marcador (00 01)
la longitud de clave de cifrado de bloque (00 00 00 20)
el tamaño del valor de seguridad (00 00 00 0c)
el tamaño de bloque de cifrado de bloque (00 00 00 10)
el tamaño de la etiqueta de autenticación (00 00 00 10) y
la etiqueta de autenticación el cifrado de bloques de ejecución (controlador de dominio E7 - final).
Administración de claves en ASP.NET Core
22/06/2018 • 12 minutes to read • Edit Online

El sistema de protección de datos administra automáticamente la duración de claves maestras de usa para
proteger y desproteger cargas. Cada clave puede estar en uno de cuatro fases:
Creado: la clave existe en el anillo de clave, pero aún no se ha activado. La clave no debe utilizarse para
nuevas operaciones de protección hasta que haya transcurrido el tiempo suficiente que la clave ha tenido
la oportunidad de propagarse a todas las máquinas que consumen este anillo de clave.
Active - la clave existe en el anillo de clave y debe utilizarse para todas las operaciones de proteger de
nuevo.
Ha caducado: la clave de su duración natural ha ejecutado y ya no debe usarse para nuevas operaciones de
protección.
Revocar - la clave está en peligro y no se debe utilizar para nuevas operaciones de protección.
Claves creadas, activas y caducadas pueden utilizarse para desproteger cargas entrantes. Claves revocadas de
forma predeterminada no pueden usarse para desproteger cargas, pero el desarrollador de aplicaciones puede
invalidar este comportamiento si es necesario.

WARNING
El programador podría verse tentado a eliminar una clave desde el anillo de clave (p. ej., eliminando el archivo
correspondiente del sistema de archivos). En ese momento, todos los datos protegidos por la clave es indescifrables
permanentemente, y no hay ninguna invalidación emergencia que hay con claves revocadas. Si se elimina una clave es un
comportamiento destructivo realmente y, por consiguiente, el sistema de protección de datos no expone ninguna API de
primera clase para realizar esta operación.

Selección de clave predeterminada


Cuando el sistema de protección de datos lee el anillo de clave desde el repositorio de respaldo, intentará
encontrar una clave de "default" desde el anillo de clave. La clave predeterminada se usa para operaciones de
proteger de nuevo.
La heurística general es que el sistema de protección de datos elige la clave con la fecha de activación más
reciente que la clave predeterminada. (No hay un factor de aglutinante pequeño para permitir el reloj del servidor
a servidor sesgo). Si la clave expiró o se revocó, generación de claves y si la aplicación no ha deshabilitado
automática, se generará una nueva clave con la activación inmediata por la clave de expiración y las sucesivas
directiva siguiente.
El motivo por el sistema de protección de datos genera una nueva clave inmediatamente en lugar de usar una
clave diferente es que la nueva generación de claves debe tratarse como una expiración implícita de todas las
claves que se activaron antes de la nueva clave. La idea general es que pueden haber sido configuradas nuevas
claves con algoritmos diferentes o mecanismos de cifrado en el resto de las claves antiguas, y el sistema debe
preferir al usar la configuración actual.
Hay una excepción. Si el desarrollador de aplicaciones tiene deshabilita la generación automática de claves, a
continuación, el sistema de protección de datos debe elegir algo como la clave predeterminada. En este escenario
de reserva, el sistema elegirá la clave no revocados con la fecha de activación más reciente, con preferencia
otorgado a las claves que hayan tenido tiempo para propagar a otros equipos del clúster. El sistema de reserva
puede acabar elegir una clave predeterminada expiradas como resultado. El sistema de reserva no elegirá nunca
una clave revocada como la clave predeterminada y si el anillo de clave está vacío o todas las claves se ha
revocado el sistema generará un error en la inicialización.

Expiración de la clave y gradual


Cuando se crea una clave, que genera automáticamente una fecha de activación de {now + 2 días} y una fecha de
expiración de {now + 90 días}. El retraso de 2 días antes de la activación le ofrece la key time en propagarse a
través del sistema. Es decir, permite que otras aplicaciones que apunta a la memoria auxiliar observar la clave en
el siguiente período de actualización automática, lo que maximiza las posibilidades de que cuando la clave de
anillo activo hace que se convierten en que se propague a todas las aplicaciones que pueden necesitar para
utilizan.
Si la clave predeterminada expirará dentro de 2 días y el anillo de clave ya no tiene una clave que se activará tras
la expiración de la clave de forma predeterminada, el sistema de protección de datos conservará
automáticamente una nueva clave para el anillo de clave. Esta nueva clave tiene una fecha de activación de {fecha
de expiración de la clave predeterminada} y una fecha de expiración de {now + 90 días}. Esto permite al sistema
poner automáticamente las claves de forma regular con ninguna interrupción del servicio.
Puede haber circunstancias donde se creará una clave con la activación inmediata. Un ejemplo sería cuando la
aplicación no se haya ejecutado durante un tiempo y todas las claves en el anillo de clave se ha caducado. Cuando
esto ocurre, la clave se genera una fecha de activación de {ahora} sin el retardo de activación normal de 2 días.
La vigencia de la clave predeterminada es 90 días, aunque esto es configurable como en el ejemplo siguiente.

services.AddDataProtection()
// use 14-day lifetime instead of 90-day lifetime
.SetDefaultKeyLifetime(TimeSpan.FromDays(14));

Un administrador también puede cambiar el valor predeterminado de todo el sistema, aunque una llamada
explícita a SetDefaultKeyLifetime invalidará cualquier directiva de todo el sistema. La vigencia de la clave
predeterminada no puede ser inferior a 7 días.

Actualización automática clave de anillo


Cuando se inicializa el sistema de protección de datos, lee el anillo de clave desde el repositorio subyacente y lo
almacena en caché en memoria. Esta memoria caché permite proteger y desproteger operaciones podrán
continuar sin alcanzar el almacén de copia de seguridad. El sistema comprobará automáticamente la memoria
auxiliar para cambios aproximadamente cada 24 horas o cuando caduca la clave predeterminada actual, lo que
ocurra primero.

WARNING
Los desarrolladores rara vez deberían (si alguna vez) que deba usar las API de administración clave directamente. El sistema
de protección de datos llevará a cabo la administración automática de claves como se describió anteriormente.

El sistema de protección de datos expone una interfaz IKeyManager que se puede utilizar para inspeccionar y
realizar cambios en el anillo de clave. El sistema DI que proporciona la instancia de IDataProtectionProvider
también puede proporcionar una instancia de IKeyManager para su consumo. Como alternativa, puede extraer el
IKeyManager directamente desde el IServiceProvider como en el ejemplo siguiente.

Cualquier operación que modifica el anillo de clave (crear una nueva clave explícitamente o realizar una
revocación) invalidará la memoria caché en memoria. La siguiente llamada a Protect o Unprotect hará que el
sistema de protección de datos leer el anillo de clave y volver a crear la memoria caché.
El ejemplo siguiente muestra cómo utilizar el IKeyManager interfaz para inspeccionar y manipular el anillo de
clave, incluida la revocación de claves existentes y generar una nueva clave manualmente.

using System;
using System.IO;
using System.Threading;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.DataProtection.KeyManagement;
using Microsoft.Extensions.DependencyInjection;

public class Program


{
public static void Main(string[] args)
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddDataProtection()
// point at a specific folder and use DPAPI to encrypt keys
.PersistKeysToFileSystem(new DirectoryInfo(@"c:\temp-keys"))
.ProtectKeysWithDpapi();
var services = serviceCollection.BuildServiceProvider();

// perform a protect operation to force the system to put at least


// one key in the key ring
services.GetDataProtector("Sample.KeyManager.v1").Protect("payload");
Console.WriteLine("Performed a protect operation.");
Thread.Sleep(2000);

// get a reference to the key manager


var keyManager = services.GetService<IKeyManager>();

// list all keys in the key ring


var allKeys = keyManager.GetAllKeys();
Console.WriteLine($"The key ring contains {allKeys.Count} key(s).");
foreach (var key in allKeys)
{
Console.WriteLine($"Key {key.KeyId:B}: Created = {key.CreationDate:u}, IsRevoked =
{key.IsRevoked}");
}

// revoke all keys in the key ring


keyManager.RevokeAllKeys(DateTimeOffset.Now, reason: "Revocation reason here.");
Console.WriteLine("Revoked all existing keys.");

// add a new key to the key ring with immediate activation and a 1-month expiration
keyManager.CreateNewKey(
activationDate: DateTimeOffset.Now,
expirationDate: DateTimeOffset.Now.AddMonths(1));
Console.WriteLine("Added a new key.");

// list all keys in the key ring


allKeys = keyManager.GetAllKeys();
Console.WriteLine($"The key ring contains {allKeys.Count} key(s).");
foreach (var key in allKeys)
{
Console.WriteLine($"Key {key.KeyId:B}: Created = {key.CreationDate:u}, IsRevoked =
{key.IsRevoked}");
}
}
}

/*
* SAMPLE OUTPUT
*
* Performed a protect operation.
* The key ring contains 1 key(s).
* Key {1b948618-be1f-440b-b204-64ff5a152552}: Created = 2015-03-18 22:20:49Z, IsRevoked = False
* Revoked all existing keys.
* Added a new key.
* Added a new key.
* The key ring contains 2 key(s).
* Key {1b948618-be1f-440b-b204-64ff5a152552}: Created = 2015-03-18 22:20:49Z, IsRevoked = True
* Key {2266fc40-e2fb-48c6-8ce2-5fde6b1493f7}: Created = 2015-03-18 22:20:51Z, IsRevoked = False
*/

Almacenamiento de claves
El sistema de protección de datos tiene una heurística mediante el cual intenta deducir automáticamente una
ubicación de almacenamiento de claves adecuado y el cifrado en el mecanismo de rest. Esto también es
configurable por el desarrollador de aplicaciones. Los documentos siguientes explican las implementaciones de
forma predeterminada de estos mecanismos:
Proveedores de almacenamiento de claves de forma predeterminada
Cifrado de claves de forma predeterminada en proveedores de rest
Proveedores de almacenamiento de claves en
ASP.NET Core
22/06/2018 • 4 minutes to read • Edit Online

De forma predeterminada, el sistema de protección de datos emplea un método heurístico para determinar
dónde se debe conservar el material de clave de cifrado. El desarrollador puede invalidar la heurística y especificar
manualmente la ubicación.

NOTE
Si especifica una ubicación de persistencia de clave explícita, el sistema de protección de datos se anular el registro el cifrado
de claves de forma predeterminada en el mecanismo de rest que proporciona la heurística, por lo que ya no se cifrarán las
claves en reposo. Se recomienda que, además especificar un mecanismo de cifrado de clave explícita para las aplicaciones de
producción.

El sistema de protección de datos que se suministra con varios proveedores de almacenamiento de claves de
forma predeterminada.

Sistema de archivos
Prevemos que muchas aplicaciones usarán un repositorio de clave basada en el sistema de archivos. Para
configurar esto, llame a la PersistKeysToFileSystem rutina de configuración tal y como se muestra a continuación.
Proporcionar un DirectoryInfo que apunta al repositorio que se deben almacenar las claves.

sc.AddDataProtection()
// persist keys to a specific directory
.PersistKeysToFileSystem(new DirectoryInfo(@"c:\temp-keys\"));

La DirectoryInfo puede apuntar a un directorio en el equipo local, o puede apuntar a una carpeta en un recurso
compartido de red. Si señala a un directorio en el equipo local (y el escenario es que sólo las aplicaciones en el
equipo local tendrá que utilizar este repositorio), considere el uso de Windows DPAPI para cifrar las claves en
reposo. En caso contrario, considere el uso de un certificado X.509 para cifrar las claves en reposo.

Azure y Redis
El Microsoft.AspNetCore.DataProtection.AzureStorage y Microsoft.AspNetCore.DataProtection.Redis paquetes
permiten almacenar las claves de protección de datos en el almacenamiento de Azure o una caché de Redis. Las
claves se pueden compartir en varias instancias de una aplicación web. La aplicación de ASP.NET Core puede
compartir las cookies de autenticación o protección CSRF en varios servidores. Para configurar en Azure, llame a
uno de los PersistKeysToAzureBlobStorage sobrecargas tal y como se muestra a continuación.

public void ConfigureServices(IServiceCollection services)


{
services.AddDataProtection()
.PersistKeysToAzureBlobStorage(new Uri("<blob URI including SAS token>"));

services.AddMvc();
}
Vea también el código de prueba de Azure.
Para configurar en Redis, llame a uno de los PersistKeysToRedis sobrecargas tal y como se muestra a
continuación.

public void ConfigureServices(IServiceCollection services)


{
// Connect to Redis database.
var redis = ConnectionMultiplexer.Connect("<URI>");
services.AddDataProtection()
.PersistKeysToRedis(redis, "DataProtection-Keys");

services.AddMvc();
}

Para obtener más información, vea las secciones siguientes:


StackExchange.Redis ConnectionMultiplexer
Caché en Redis de Azure
Código de prueba de Redis.

Registro
A veces, la aplicación podría no tener acceso de escritura al sistema de archivos. Considere un escenario donde se
ejecuta una aplicación como una cuenta de servicio virtual (por ejemplo, la identidad del grupo de aplicaciones de
w3wp.exe). En estos casos, el administrador puede haber aprovisionado una clave del registro que sea adecuado
con las ACL para la identidad de la cuenta de servicio. Llame a la PersistKeysToRegistry rutina de configuración
tal y como se muestra a continuación. Proporcionar un RegistryKey que apunta a la ubicación donde se deben
almacenar valores/claves criptográficos.

sc.AddDataProtection()
// persist keys to a specific location in the system registry
.PersistKeysToRegistry(Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Sample\keys"));

Si usa el registro del sistema como un mecanismo de persistencia, considere el uso de Windows DPAPI para
cifrar las claves en reposo.

Repositorio de clave personalizado


Si no resultan adecuados los mecanismos de forma predeterminada, el programador puede especificar su propio
mecanismo de persistencia de clave proporcionando un personalizado IXmlRepository .
Cifrado de claves en reposo en ASP.NET Core
22/06/2018 • 7 minutes to read • Edit Online

De forma predeterminada, el sistema de protección de datos emplea un método heurístico para determinar cómo
criptográfico material de clave debe cifrarse en reposo. El desarrollador puede invalidar la heurística y especificar
manualmente cómo se deben cifrar las claves en reposo.

NOTE
Si especifica un cifrado de clave explícita en el mecanismo de rest, el sistema de protección de datos se anular el registro el
mecanismo de almacenamiento de claves predeterminado que proporciona la heurística. Debe especifican un mecanismo de
almacenamiento de claves explícitas, en caso contrario, no podrá iniciar el sistema de protección de datos.

El sistema de protección de datos que se suministra con tres mecanismos de cifrado de claves de forma
predeterminada.

DPAPI de Windows
Este mecanismo solo está disponible en Windows.
Cuando se utiliza DPAPI de Windows, material de clave se cifrará a través de CryptProtectData antes de que se
conservan en el almacenamiento. DPAPI es un mecanismo de cifrado adecuado para los datos que nunca se
leerán fuera de la máquina actual (aunque es posible hacer copia de estas claves en Active Directory; vea DPAPI y
perfiles móviles). Por ejemplo configurar el cifrado de clave en el resto DPAPI.

sc.AddDataProtection()
// only the local user account can decrypt the keys
.ProtectKeysWithDpapi();

Si ProtectKeysWithDpapi se llaman sin ningún parámetro, sólo la cuenta de usuario de Windows actual puede
descifrar el material de clave persistente. Opcionalmente, puede especificar que cualquier cuenta de usuario en el
equipo (no solo la cuenta de usuario actual) debe ser capaz de descifrar el material de clave, como se muestra en
el ejemplo siguiente.

sc.AddDataProtection()
// all user accounts on the machine can decrypt the keys
.ProtectKeysWithDpapi(protectToLocalMachine: true);

Certificado X.509
Este mecanismo no está disponible en .NET Core 1.0 o 1.1 .
Si la aplicación se reparte entre varias máquinas, puede ser conveniente para distribuir un certificado X.509
compartido entre las máquinas y configurar aplicaciones para usar este certificado para el cifrado de claves en
reposo. Vea a continuación para obtener un ejemplo.

sc.AddDataProtection()
// searches the cert store for the cert with this thumbprint
.ProtectKeysWithCertificate("3BCE558E2AD3E0E34A7743EAB5AEA2A9BD2575A0");
Debido a limitaciones de .NET Framework se admiten sólo los certificados con claves privadas de CAPI. Vea
basada en certificados de cifrado con Windows DPAPI-NG a continuación para buscar posibles soluciones para
estas limitaciones.

Windows DPAPI-NG
Este mecanismo solo está disponible en Windows 8 / Windows Server 2012 y versiones posterior.
A partir de Windows 8, el sistema operativo admite DPAPI-NG (también denominado CNG DPAPI). Microsoft
distribuye su escenario de uso como se indica a continuación.
Informática en nube, sin embargo, a menudo requiere que ese contenido cifrado en un equipo pueden descifrar
en otro. Por lo tanto, a partir de Windows 8, Microsoft ampliado la idea de usar una API relativamente sencilla
para abarcar escenarios de nube. Esta nueva API, denominada DPAPI-NG, permite compartir de forma segura
secretos (claves, contraseñas, material de clave) y mensajes protegiéndolos a un conjunto de entidades de
seguridad que puede utilizarse para desproteger en equipos diferentes después de la autorización y la
autenticación correcta.
Desde sobre DPAPI CNG
La entidad de seguridad se codifica como una regla de descriptor de protección. Considere el ejemplo siguiente,
que cifra el material de clave de modo que solo el usuario unido al dominio con el SID especificado puede
descifrar el material de clave.

sc.AddDataProtection()
// uses the descriptor rule "SID=S-1-5-21-..."
.ProtectKeysWithDpapiNG("SID=S-1-5-21-...",
flags: DpapiNGProtectionDescriptorFlags.None);

También hay una sobrecarga sin parámetros de ProtectKeysWithDpapiNG . Se trata de un método útil para
especificar la regla "SID = mío", donde la extraiga es el SID de la cuenta de usuario de Windows actual.

sc.AddDataProtection()
// uses the descriptor rule "SID={current account SID}"
.ProtectKeysWithDpapiNG();

En este escenario, el controlador de dominio de AD es responsable de distribuir las claves de cifrado que se utiliza
en las operaciones de NG DPAPI. El usuario de destino podrá descifrar la carga cifrada desde cualquier equipo
unido al dominio (siempre que el proceso se ejecuta bajo su identidad).

Cifrado basada en certificados con Windows DPAPI-NG


Si se está ejecutando en Windows 8.1 / Windows Server 2012 R2 o versiones posteriores, puede usar Windows
DPAPI-NG para realizar el cifrado basada en certificados, incluso si la aplicación se ejecuta en .NET Core. Para
aprovechar estas características, utilice la cadena de descriptor de la regla "certificado = HashId:thumbprint",
donde la huella digital es la huella digital con codificación hexadecimal SHA1 del certificado que se va a usar. Vea
a continuación para obtener un ejemplo.

sc.AddDataProtection()
// searches the cert store for the cert with this thumbprint
.ProtectKeysWithDpapiNG("CERTIFICATE=HashId:3BCE558E2AD3E0E34A7743EAB5AEA2A9BD2575A0",
flags: DpapiNGProtectionDescriptorFlags.None);

Cualquier aplicación que se señala en este repositorio debe ejecutarse en Windows 8.1 / Windows Server 2012
R2 o posterior para que pueda descifrar esta clave.
Cifrado de clave personalizado
Si no resultan adecuados los mecanismos de forma predeterminada, el programador puede especificar su propio
mecanismo de cifrado de claves proporcionando un personalizado IXmlEncryptor .
Inmutabilidad de clave y la configuración de clave de
ASP.NET Core
22/06/2018 • 2 minutes to read • Edit Online

Una vez que un objeto se mantiene en la memoria auxiliar, su representación de infinito es fijo. Se pueden agregar
datos nuevos a la memoria auxiliar, pero nunca pueden transformarse los datos existentes. El propósito principal
de este comportamiento es evitar daños en los datos.
Una consecuencia de este comportamiento es que, cuando una clave se escribe en la memoria auxiliar, es
inmutable. Su fecha de creación, activación y expiración nunca se puede cambiar, aunque puede revocar utilizando
IKeyManager . Además, su información algorítmica subyacente, el material de creación de claves maestras y el
cifrado en Propiedades de rest también son inmutables.
Si el desarrollador cambia cualquier configuración que afecta a la persistencia de clave, esos cambios no entran en
vigor hasta la próxima vez que se genere una clave, ya sea a través de una llamada explícita a
IKeyManager.CreateNewKey o a través de lo datos protección del sistema propio clave automática generación
comportamiento. La configuración que afecta a la persistencia de clave es los siguientes:
La vigencia de clave predeterminada
El cifrado de claves en el mecanismo de rest
La información algorítmica contenida dentro de la clave
Si necesita estas opciones para iniciar anteriores a la siguiente tecla automática gradual tiempo, considere la
posibilidad de realizar una llamada explícita a IKeyManager.CreateNewKey para forzar la creación de una nueva
clave. Recuerde que debe proporcionar una fecha de activación explícita ({ahora + 2 días} es una buena regla
general para dejar tiempo propagar el cambio) y la fecha de expiración en la llamada.

TIP
Todas las aplicaciones tocar el repositorio deben especificar la misma configuración con el IDataProtectionBuilder
métodos de extensión. De lo contrario, las propiedades de la clave persistente será depende de la aplicación en particular
que invoca las rutinas de generación de claves.
Formato de almacenamiento de claves en ASP.NET
Core
22/06/2018 • 5 minutes to read • Edit Online

Objetos se almacenan en reposo en la representación XML. El directorio predeterminado para el almacenamiento


de claves es % LOCAL APPDATA%\ASP.NET\DataProtection-Keys.

El <clave > elemento


Las claves existen como objetos de nivel superior en el repositorio de clave. Por convención, las claves tienen el
nombre de archivo clave-{guid} .xml, donde {guid} es el identificador de la clave. Cada archivo de este tipo
contiene una clave única. El formato del archivo es como sigue.

<?xml version="1.0" encoding="utf-8"?>


<key id="80732141-ec8f-4b80-af9c-c4d2d1ff8901" version="1">
<creationDate>2015-03-19T23:32:02.3949887Z</creationDate>
<activationDate>2015-03-19T23:32:02.3839429Z</activationDate>
<expirationDate>2015-06-17T23:32:02.3839429Z</expirationDate>
<descriptor deserializerType="{deserializerType}">
<descriptor>
<encryption algorithm="AES_256_CBC" />
<validation algorithm="HMACSHA256" />
<enc:encryptedSecret decryptorType="{decryptorType}" xmlns:enc="...">
<encryptedKey>
<!-- This key is encrypted with Windows DPAPI. -->
<value>AQAAANCM...8/zeP8lcwAg==</value>
</encryptedKey>
</enc:encryptedSecret>
</descriptor>
</descriptor>
</key>

El <clave > elemento contiene los siguientes atributos y elementos secundarios:


El identificador de clave. Este valor se trata como autorizados; el nombre de archivo es simplemente una
nicety más legible.
La versión de la <clave > elemento, que actualmente se fija en 1.
Fecha de creación, activación y expiración de la clave.
Un <descriptor > elemento, que contiene información sobre la implementación del cifrado autenticado
dentro de esta clave.
En el ejemplo anterior, Id. de la clave es {80732141-ec8f-4b80-af9c-c4d2d1ff8901}, que se creó o se activa en el
19 de marzo de 2015 y tiene una duración de 90 días. (En ocasiones, la fecha de activación puede estar
ligeramente antes de la fecha de creación como en este ejemplo. Esto es debido a un nit en la forma en que las
API de trabajo y es inofensivas en la práctica).

El <descriptor > elemento


El exterior <descriptor > elemento contiene un deserializerType de atributo, que es el nombre calificado con el
ensamblado de un tipo que implementa IAuthenticatedEncryptorDescriptorDeserializer. Este tipo es responsable
de leer interna <descriptor > elemento y para analizar la información incluida en.
El formato en cuestión de la <descriptor > elemento depende de la implementación de sistema de cifrado
autenticado encapsulada por la clave y cada tipo de deserializador espera un formato ligeramente diferente para
este. En general, no obstante, este elemento contendrá información algorítmica (nombres, tipos, OID, o similar) y
material de clave secreta. En el ejemplo anterior, el descriptor especifica que ajuste esta clave de cifrado de AES -
256-CBC + HMACSHA256 validación.

El <encryptedSecret > elemento


Un elemento que contiene la forma cifrada del material de clave secreta puede estar presente si está habilitado el
cifrado de secretos en reposo. El atributo decryptorType será el nombre calificado con el ensamblado de un tipo
que implementa IXmlDecryptor. Este tipo es responsable de leer interna elemento y el descifrado para recuperar
el texto sin formato original.
Al igual que con <descriptor >, el formato en cuestión de la elemento depende el mecanismo de cifrado en reposo
en uso. En el ejemplo anterior, la clave maestra se cifra mediante DPAPI de Windows por el comentario.

El <revocación > elemento


Revocaciones existen como objetos de nivel superior en el repositorio de clave. Por convención, las revocaciones
tienen el nombre de archivo revocación-{timestamp} .xml (para revocar todas las claves antes de una fecha
concreta) o revocación-{guid} .xml (para revocar una clave específica). Cada archivo contiene un único
<revocación > elemento.
Para las revocaciones de las claves individuales, será el contenido del archivo a la siguiente.

<?xml version="1.0" encoding="utf-8"?>


<revocation version="1">
<revocationDate>2015-03-20T22:45:30.2616742Z</revocationDate>
<key id="eb4fc299-8808-409d-8a34-23fc83d026c9" />
<reason>human-readable reason</reason>
</revocation>

En este caso, solo la clave especificada se ha revocado. Si el identificador de clave es "*", sin embargo, como en el
ejemplo siguiente, se revocan todas las claves cuya fecha de creación es anterior a la fecha de revocación
especificada.

<?xml version="1.0" encoding="utf-8"?>


<revocation version="1">
<revocationDate>2015-03-20T15:45:45.7366491-07:00</revocationDate>
<!-- All keys created before the revocation date are revoked. -->
<key id="*" />
<reason>human-readable reason</reason>
</revocation>

El <motivo > elemento nunca se lee por el sistema. Es simplemente un lugar conveniente para almacenar un
motivo de revocación legible.
Proveedores de protección de datos efímero en
ASP.NET Core
22/06/2018 • 2 minutes to read • Edit Online

Existen escenarios donde una aplicación necesita un throwaway IDataProtectionProvider . Por ejemplo, solo se
puede experimentar el desarrollador en una aplicación de consola de uso único o la propia aplicación es transitoria
(se incluye en el script o una prueba unitaria de proyecto). Para admitir estos escenarios el
Microsoft.AspNetCore.DataProtection paquete incluye un tipo EphemeralDataProtectionProvider . Este tipo
proporciona una implementación básica de IDataProtectionProvider cuya clave repositorio se mantiene
solamente en memoria y no escribe en ningún almacén de respaldo.
Cada instancia de EphemeralDataProtectionProvider usa su propia clave principal único. Por lo tanto, si un
IDataProtector con raíz en un EphemeralDataProtectionProvider genera una carga protegida, ese carga solo puede
desproteger un equivalente IDataProtector (les proporciona el mismo propósito cadena) con raíz en el mismo
EphemeralDataProtectionProvider instancia.

El siguiente ejemplo muestra cómo crear instancias de un EphemeralDataProtectionProvider y usarla para proteger
y desproteger los datos.
using System;
using Microsoft.AspNetCore.DataProtection;

public class Program


{
public static void Main(string[] args)
{
const string purpose = "Ephemeral.App.v1";

// create an ephemeral provider and demonstrate that it can round-trip a payload


var provider = new EphemeralDataProtectionProvider();
var protector = provider.CreateProtector(purpose);
Console.Write("Enter input: ");
string input = Console.ReadLine();

// protect the payload


string protectedPayload = protector.Protect(input);
Console.WriteLine($"Protect returned: {protectedPayload}");

// unprotect the payload


string unprotectedPayload = protector.Unprotect(protectedPayload);
Console.WriteLine($"Unprotect returned: {unprotectedPayload}");

// if I create a new ephemeral provider, it won't be able to unprotect existing


// payloads, even if I specify the same purpose
provider = new EphemeralDataProtectionProvider();
protector = provider.CreateProtector(purpose);
unprotectedPayload = protector.Unprotect(protectedPayload); // THROWS
}
}

/*
* SAMPLE OUTPUT
*
* Enter input: Hello!
* Protect returned: CfDJ8AAAAAAAAAAAAAAAAAAAAA...uGoxWLjGKtm1SkNACQ
* Unprotect returned: Hello!
* << throws CryptographicException >>
*/
Compatibilidad en ASP.NET Core
21/06/2018 • 2 minutes to read • Edit Online

Reemplazar <machineKey> de ASP.NET en ASP.NET Core


Reemplace el elemento machineKey ASP.NET en
ASP.NET Core
22/06/2018 • 4 minutes to read • Edit Online

La implementación de la <machineKey> elemento en ASP.NET es reemplazable. Esto permite la mayoría de las


llamadas a rutinas criptográficas de ASP.NET se enruten a través de un mecanismo de protección de datos de
reemplazo, incluido el nuevo sistema de protección de datos.

Instalación del paquete


NOTE
El nuevo sistema de protección de datos solo puede instalarse en una aplicación ASP.NET existente como destino .NET 4.5.1 o
posterior. Instalación se producirá un error si la aplicación tiene como destino .NET 4.5 o Bajar.

Para instalar el nuevo sistema de protección de datos en un proyecto de 4.5.1+ ASP.NET existente, instale el
paquete Microsoft.AspNetCore.DataProtection.SystemWeb. Esto creará una instancia del sistema de protección
de datos mediante la configuración predeterminada configuración.
Cuando se instala el paquete, inserta una línea en Web.config que le indica a ASP.NET para usarla para más
operaciones criptográficas, como la autenticación de formularios, estado de vista y llamadas a MachineKey.Protect.
La línea que se inserta quede como sigue.

<machineKey compatibilityMode="Framework45" dataProtectorType="..." />

TIP
Puede indicar si el nuevo sistema de protección de datos está activo mediante la inspección de campos como __VIEWSTATE ,
que debe comenzar por "CfDJ8" en el ejemplo siguiente. "CfDJ8" es la representación base64 del encabezado de magia "09
F0 C9 F0" que identifica una carga protegida por el sistema de protección de datos.

<input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value="CfDJ8AWPr2EQPTBGs3L2GCZOpk..." />

Configuración de paquetes
El sistema de protección de datos se crea una instancia con una configuración predeterminada del programa de
instalación de cero. Sin embargo, puesto que de forma predeterminada, las claves se conservan al sistema de
archivos local, esto no funcionará para las aplicaciones que se implementan en una granja de servidores. Para
resolver este problema, puede proporcionar la configuración mediante la creación de un tipo que las subclases
DataProtectionStartup e invalida su método ConfigureServices.
A continuación se muestra un ejemplo de un tipo de inicio de protección de datos personalizado que configura
tanto donde se conservan las claves, y cómo está cifrados en reposo. También invalida la directiva de aislamiento
de aplicaciones predeterminado proporcionando su propio nombre de la aplicación.
using System;
using System.IO;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.DataProtection.SystemWeb;
using Microsoft.Extensions.DependencyInjection;

namespace DataProtectionDemo
{
public class MyDataProtectionStartup : DataProtectionStartup
{
public override void ConfigureServices(IServiceCollection services)
{
services.AddDataProtection()
.SetApplicationName("my-app")
.PersistKeysToFileSystem(new DirectoryInfo(@"\\server\share\myapp-keys\"))
.ProtectKeysWithCertificate("thumbprint");
}
}
}

TIP
También puede usar <machineKey applicationName="my-app" ... /> en lugar de una llamada explícita a
SetApplicationName. Se trata de un mecanismo de comodidad para evitar la fuerza al desarrollador para crear un tipo
derivado de DataProtectionStartup si todos los que deseaban configurar se establecen el nombre de la aplicación.

Para habilitar esta configuración personalizada, vuelva al archivo Web.config y busque la <appSettings> elemento
que instalar el paquete agregado al archivo de configuración. Tendrá una apariencia similar el siguiente marcado:

<appSettings>
<!--
If you want to customize the behavior of the ASP.NET Core Data Protection stack, set the
"aspnet:dataProtectionStartupType" switch below to be the fully-qualified name of a
type which subclasses Microsoft.AspNetCore.DataProtection.SystemWeb.DataProtectionStartup.
-->
<add key="aspnet:dataProtectionStartupType" value="" />
</appSettings>

Rellene el valor en blanco con el nombre completo de ensamblado del tipo derivado de DataProtectionStartup
que acaba de crear. Si el nombre de la aplicación es DataProtectionDemo, esto sería el siguiente.

<add key="aspnet:dataProtectionStartupType"
value="DataProtectionDemo.MyDataProtectionStartup, DataProtectionDemo" />

El sistema de protección de datos recién configurada ahora está listo para su uso dentro de la aplicación.
Exigir HTTPS en el núcleo de ASP.NET
22/06/2018 • 9 minutes to read • Edit Online

Por Rick Anderson


Este documento se muestra cómo:
Requerir HTTPS para todas las solicitudes.
Redirigir todas las solicitudes HTTP a HTTPS.

WARNING
Hacer no usar RequireHttpsAttribute en las API Web que reciben información confidencial. RequireHttpsAttribute usa
códigos de estado HTTP para redirigir exploradores de HTTP a HTTPS. Los clientes de API no pueden entender o siguen las
redirecciones de HTTP a HTTPS. Estos clientes pueden enviar información a través de HTTP. Las API Web deben realizar las
tareas:
No escuchar en HTTP.
Cierre la conexión con el código de estado 400 (solicitud incorrecta) y no atender la solicitud.

Requerir HTTPS
Se recomienda que todas las aplicaciones web de ASP.NET Core llamará Middleware de redirección de HTTPS
(UseHttpsRedirection) para redirigir todas las solicitudes HTTP a HTTPS.
El código siguiente llama UseHttpsRedirection en la Startup clase:
[!code-csharp]
El código siguiente llama AddHttpsRedirection para configurar las opciones de middleware:
[!code-csharp]
El código resaltado anterior:
Conjuntos de HttpsRedirectionOptions.RedirectStatusCode a Status307TemporaryRedirect , que es el valor
predeterminado. Deben llamar aplicaciones de producción UseHsts.
Establece el puerto HTTPS en 5001. El valor predeterminado es 443.
Los siguientes mecanismos establecen automáticamente el puerto:
El software intermedio puede detectar los puertos a través de IServerAddressesFeature cuando se aplican
las condiciones siguientes:
Se utiliza kestrel o HTTP.sys directamente con los puntos de conexión HTTPS (también se aplica a la
aplicación se ejecuta con el depurador de código de Visual Studio).
Solo un puerto HTTPS se utiliza la aplicación.
Se utiliza Visual Studio:
IIS Express tiene habilitados para HTTPS.
launchSettings.json establece el sslPort de IIS Express.
NOTE
Cuando una aplicación se ejecute detrás de un proxy inverso (por ejemplo, IIS, IIS Express), IServerAddressesFeature no
está disponible. El puerto debe configurarse manualmente. Cuando el puerto no está configurado, no se redirigen las
solicitudes.

El puerto se puede configurar estableciendo el:


La variable de entorno ASPNETCORE_HTTPS_PORT .
http_port clave de configuración de host (por ejemplo, a través de hostsettings.json o un argumento de línea
de comandos).
HttpsRedirectionOptions.HttpsPort. Vea el ejemplo anterior que se muestra cómo establecer el puerto a
5001.

NOTE
El puerto puede configurarse indirectamente estableciendo la dirección URL con el ASPNETCORE_URLS variable de entorno.
La variable de entorno configura el servidor y, a continuación, el middleware indirectamente detecta el puerto HTTPS a
través de IServerAddressesFeature .

Si no se establece ningún puerto:


No se redirigen las solicitudes.
El middleware registra una advertencia.

NOTE
Una alternativa al uso de Middleware de redirección de HTTPS ( UseHttpsRedirection ) consiste en usar el Middleware de
reescritura de dirección URL ( AddRedirectToHttps ). AddRedirectToHttps puede establecer el código de estado y el
puerto cuando se ejecuta la redirección. Para obtener más información, consulte Middleware de reescritura de dirección
URL.
Al redirigir a HTTPS sin necesidad de reglas de redirección adicionales, se recomienda usar HTTPS redirección Middleware (
UseHttpsRedirection ) se describe en este tema.

El RequireHttpsAttribute se usa para requerir HTTPS. [RequireHttpsAttribute] puede decorar controladores o


métodos, o se pueden aplicar globalmente. Para aplicar el atributo global, agregue el código siguiente a
ConfigureServices en Startup :

[!code-csharp]
El código resaltado anterior requiere que todas las solicitudes usar HTTPS ; por lo tanto, se omiten las solicitudes
HTTP. El código resaltado siguiente redirige todas las solicitudes HTTP a HTTPS:
[!code-csharp]
Para obtener más información, consulte Middleware de reescritura de dirección URL. El middleware también
permite a la aplicación para establecer el código de estado o el código de estado y el puerto cuando se ejecuta la
redirección.
Requerir HTTPS globalmente ( options.Filters.Add(new RequireHttpsAttribute()); ) es una práctica
recomendada de seguridad. Aplicar el [RequireHttps] atributo a todas las páginas de Razor/controladores no
considera tan seguro como requerir HTTPS globalmente. No puede garantizar la [RequireHttps] atributo se
aplica cuando se agregan nuevos controladores y las páginas de Razor.
Protocolo de seguridad de transporte estrictos de HTTP (HSTS)
Por OWASP, seguridad de transporte estricta de HTTP (HSTS ) supone una mejora de seguridad opcional que
se especifica mediante una aplicación web mediante el uso de un encabezado de respuesta especial. Una vez
que un explorador compatible recibe este encabezado ese explorador impide que todas las comunicaciones se
envíen a través de HTTP para el dominio especificado y en su lugar, le enviará todas las comunicaciones a través
de HTTPS. También evita que haga clic en HTTPS a través de solicitudes en exploradores.
ASP.NET Core 2.1 o posterior implementa HSTS con el UseHsts método de extensión. El código siguiente llama
UseHsts cuando la aplicación no se encuentra en modo de desarrollo:

[!code-csharp]
UseHsts no se recomienda en el desarrollo porque el encabezado HSTS es alta almacenable en caché
exploradores. De forma predeterminada, UseHsts excluye la dirección de bucle invertido local.
El código siguiente:
[!code-csharp]
Establece el parámetro precarga del encabezado de seguridad de transporte Strict. Precarga no es parte de la
especificación RFC HSTS, pero es compatible con los exploradores web para cargar previamente sitios HSTS
en instalación nueva. Para más información, vea https://hstspreload.org/.
Permite includeSubDomain, que aplica la directiva HSTS a subdominios de Host.
Establece explícitamente el parámetro de max-age del encabezado de seguridad de transporte Strict a 60
días. Si no se establece, el valor predeterminado es 30 días. Consulte la max-age directiva para obtener más
información.
Agrega example.com a la lista de hosts que se van a excluir.
UseHsts excluye los siguientes hosts de bucle invertido:
localhost : La dirección de bucle invertido de IPv4.
127.0.0.1 : La dirección de bucle invertido de IPv4.
[::1] : La dirección de bucle invertido de IPv6.

En el ejemplo anterior se muestra cómo agregar hosts adicionales.

Desactivación de HTTPS en la creación del proyecto


Habilitar las plantillas de aplicación de ASP.NET Core web 2.1 o posterior (en Visual Studio o la línea de
comandos dotnet) redirección HTTPS y HSTS. Para las implementaciones que no requieran HTTPS, puede
cancelar voluntariamente la suscripción de HTTPS. Por ejemplo, algunos servicios back-end donde HTTPS se
está controlando externamente en el perímetro, mediante HTTPS en cada nodo no es necesario.
A la cancelación de HTTPS:
Visual Studio
CLI de .NET Core
Desactive el configurar para HTTPS casilla de verificación.
Cómo configurar un certificado de desarrollador para Docker
Vea este problema de GitHub.
Compatibilidad de la UE General datos protección
normativa (GDPR) en ASP.NET Core
22/06/2018 • 9 minutes to read • Edit Online

Por Rick Anderson


ASP.NET Core proporciona las API y plantillas para ayudar a cumplir algunos de los normativa General de
protección de datos (GDPR ) de la UE requisitos:
Las plantillas de proyecto incluyen puntos de extensión y marcado auxiliar que puede reemplazar por su
privacidad y la directiva de uso de cookies.
Una característica de consentimiento de cookie permite pedir consentimiento (y realizar un seguimiento) de
los usuarios para almacenar la información personal. Si un usuario no ha dado su consentimiento para la
recopilación de datos y la aplicación está configurada con CheckConsentNeeded a true , las cookies no sean
esenciales no se enviará al explorador.
Las cookies se pueden marcar como esenciales. Las cookies esenciales se envían al explorador incluso cuando
el usuario no ha dado su consentimiento y seguimiento está deshabilitado.
Las cookies de sesión y TempData no son funcionales cuando el seguimiento está deshabilitado.
El administrar identidades página proporciona un vínculo para descargar y eliminar datos de usuario.
El aplicación de ejemplo le permite probar la mayoría de las API que se agregará a las plantillas de ASP.NET Core
2.1 y puntos de extensión GDPR. Consulte la Léame un archivo para probar las instrucciones.
Vea o descargue el código de ejemplo (cómo descargarlo)

Código generado por el soporte técnico GDPR de ASP.NET Core en


plantilla
Las páginas de Razor y MVC proyectos creados con las plantillas de proyecto incluyen la compatibilidad GDPR
siguiente:
CookiePolicyOptions y UseCookiePolicy se establecen en Startup .
El _CookieConsentPartial.cshtml vista parcial.
El Pages/Privacy.cshtml o Home/Privacy.cshtml vista proporciona una página de detalle de la directiva de
privacidad de su sitio. El _CookieConsentPartial.cshtml archivo genera un vínculo a la página de privacidad.
Para las aplicaciones creadas con cuentas de usuario individuales, la página de administración proporciona
vínculos para descargar y eliminar personal del usuario.
CookiePolicyOptions y UseCookiePolicy
CookiePolicyOptions se inicializan en el Startup clase ConfigureServices método:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}

public IConfiguration Configuration { get; }

// This method gets called by the runtime. Use this method to add services
// to the container.
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies
// is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});

services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<IdentityUser>()
.AddEntityFrameworkStores<ApplicationDbContext>();

services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}

// This method gets called by the runtime. Use this method to configure the
// HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();

app.UseAuthentication();

app.UseMvc();
}
}

UseCookiePolicy se llama el Startup clase Configure método:


public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}

public IConfiguration Configuration { get; }

// This method gets called by the runtime. Use this method to add services
// to the container.
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies
// is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});

services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<IdentityUser>()
.AddEntityFrameworkStores<ApplicationDbContext>();

services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}

// This method gets called by the runtime. Use this method to configure the
// HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();

app.UseAuthentication();

app.UseMvc();
}
}

Vista parcial _CookieConsentPartial.cshtml


El _CookieConsentPartial.cshtml vista parcial:
@using Microsoft.AspNetCore.Http.Features

@{
var consentFeature = Context.Features.Get<ITrackingConsentFeature>();
var showBanner = !consentFeature?.CanTrack ?? false;
var cookieString = consentFeature?.CreateConsentCookie();
}

@if (showBanner)
{
<nav id="cookieConsent" class="navbar navbar-default navbar-fixed-top" role="alert">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-
target="#cookieConsent .navbar-collapse">
<span class="sr-only">Toggle cookie consent banner</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<span class="navbar-brand"><span class="glyphicon glyphicon-info-sign" aria-hidden="true">
</span></span>
</div>
<div class="collapse navbar-collapse">
<p class="navbar-text">
Use this space to summarize your privacy and cookie use policy.
</p>
<div class="navbar-right">
<a asp-page="/Privacy" class="btn btn-info navbar-btn">Learn More</a>
<button type="button" class="btn btn-default navbar-btn" data-cookie-
string="@cookieString">Accept</button>
</div>
</div>
</div>
</nav>
<script>
(function () {
document.querySelector("#cookieConsent button[data-cookie-string]").addEventListener("click",
function (el) {
document.cookie = el.target.dataset.cookieString;
document.querySelector("#cookieConsent").classList.add("hidden");
}, false);
})();
</script>
}

Este parcial:
Obtiene el estado de seguimiento para el usuario. Si la aplicación se configura para requerir el consentimiento
que del usuario debe dar su consentimiento antes de que pueden realizar el seguimiento de las cookies. Si se
requiere consentimiento, el cromo de consentimiento de la cookie se fija en la barra de navegación que se creó
en la parte superior del Pages/Shared/_Layout.cshtml archivo.
Proporciona una etiqueta HTML <p> usar la directiva de elemento que se va a resumir su privacidad y
cookies.
Proporciona un vínculo a Pages/Privacy.cshtml donde puede detallar política de privacidad de su sitio.

Cookies esenciales
Si no se ha proporcionado el consentimiento, solo las cookies de marcado esenciales se envían al explorador. El
código siguiente realiza una cookie esenciales:
public IActionResult OnPostCreateEssentialAsync()
{
HttpContext.Response.Cookies.Append(Constants.EssentialSec,
DateTime.Now.Second.ToString(),
new CookieOptions() { IsEssential = true });

ResponseCookies = Response.Headers[HeaderNames.SetCookie].ToString();

return RedirectToPage("./Index");
}

Cookies de estado de sesión y el proveedor TempData no sean


esenciales
El proveedor Tempdata cookie no es esencial. Si se deshabilita el seguimiento, el proveedor Tempdata no es
funcional. Para habilitar el proveedor Tempdata cuando el seguimiento está deshabilitado, marcar la cookie de
TempData como esenciales en ConfigureServices :

// The Tempdata provider cookie is not essential. Make it essential


// so Tempdata is functional when tracking is disabled.
services.Configure<CookieTempDataProviderOptions>(options => {
options.Cookie.IsEssential = true;
});

Estado de sesión cookies no son esenciales. Estado de sesión no es funcional cuando se deshabilita el
seguimiento.

Datos personales
Las aplicaciones de ASP.NET Core creadas con cuentas de usuario individuales incluyen código para descargar y
eliminar datos personales.
Seleccione el nombre de usuario y, a continuación, seleccione datos personales:
Notas:
Para generar el Account/Manage código, vea Scaffold identidad.
Eliminar y descargar impacto solamente los datos de identidad de manera predeterminada. Los datos de
usuario personalizado de creación de aplicaciones deben extenderse para delete/descargar los datos de
usuario personalizada. Problema de GitHub cómo agregar o eliminar datos de usuario personalizado a la
identidad realiza un seguimiento de un artículo propuesto acerca de cómo crear
personalizado/eliminar/descarga de datos de usuario personalizada. Si le gustaría que ese tema un nivel de
prioridad, deje un Pulgar hacia arriba reacción en el problema.
Guarda los tokens para el usuario que se almacenan en la tabla de base de datos de identidad
AspNetUserTokens se eliminan cuando el usuario se elimina mediante el comportamiento de eliminación en
cascada debido a la clave externa.

Cifrado en reposo
Algunas bases de datos y mecanismos de almacenamiento permiten para el cifrado en reposo. Cifrado en reposo:
Cifra automáticamente los datos almacenados.
Cifra sin configuración, programación u otras tareas para el software que tiene acceso a los datos.
Es la opción más sencilla y más segura.
Permite que la base de datos administre las claves y el cifrado.
Por ejemplo:
Microsoft SQL y SQL Azure proporcionan cifrado de datos transparente (TDE ).
SQL Azure cifra la base de datos de forma predeterminada
Azure Blobs, archivos, tabla y cola de almacenamiento se cifran de forma predeterminada.
Para las bases de datos que no proporcionan cifrado integrado en reposo, es posible que pueda usar el cifrado del
disco para proporcionar la misma protección. Por ejemplo:
BitLocker para Windows Server
Linux:
eCryptfs
EncFS.

Recursos adicionales
Microsoft.com/GDPR
Ubicación de almacenamiento segura de
secretos de la aplicación en el desarrollo de
ASP.NET Core
23/06/2018 • 14 minutes to read • Edit Online

Por Rick Anderson, Daniel Roth, y Scott Addie


Vea o descargue el código de ejemplo (cómo descargarlo)
Este documento explica técnicas para almacenar y recuperar datos confidenciales durante el desarrollo de
una aplicación de ASP.NET Core. Nunca debe almacenar las contraseñas u otros datos confidenciales en
el código fuente, y no debe utilizar secretos de producción en el desarrollo o modo de prueba. Puede
almacenar y proteger los secretos de prueba y producción Azure con el proveedor de configuración de
almacén de claves de Azure.

Variables de entorno
Las variables de entorno se utilizan para evitar el almacenamiento de secretos de la aplicación en el
código o en archivos de configuración local. Las variables de entorno invalidan los valores de
configuración de todos los orígenes de configuración especificada anteriormente.
Configurar la lectura de los valores de variables de entorno mediante una llamada a
AddEnvironmentVariables en el Startup constructor:
[!code-csharp]
Piense en una aplicación web de ASP.NET Core en la que cuentas de usuario individuales está
habilitada la seguridad. Una cadena de conexión de base de datos predeterminada se incluye en el
proyecto appSettings.JSON que se archivo con la clave DefaultConnection . La cadena de conexión
predeterminada es LocalDB, que se ejecuta en modo de usuario y no requiere una contraseña. Durante la
implementación de aplicaciones, el DefaultConnection clave-valor puede reemplazarse por el valor de la
variable de entorno. La variable de entorno puede almacenar la cadena de conexión completa con las
credenciales confidenciales.

WARNING
Las variables de entorno se almacenan normalmente en texto sin formato y sin cifrar. Si la máquina o el proceso se
ve comprometido, confianza pueden tener acceso a las variables de entorno. Saber qué medidas adicionales para
evitar la divulgación de información confidencial del usuario pueden ser necesarias.

Administrador de secreto
La herramienta Administrador de secreto almacena datos confidenciales durante el desarrollo de un
proyecto de ASP.NET Core. En este contexto, una parte de los datos confidenciales es un secreto de la
aplicación. Secretos de la aplicación se almacenan en una ubicación independiente desde el árbol del
proyecto. Los secretos de la aplicación se asociada a un proyecto específico o se comparten entre varios
proyectos. Los secretos de aplicación no se protegen en el control de origen.
WARNING
La herramienta Administrador de secreto no cifra los secretos almacenados y no debe tratarse como un almacén
de confianza. Es solo con fines de desarrollo. Las claves y los valores se almacenan en un archivo de configuración
de JSON en el directorio del perfil de usuario.

Cómo funciona la herramienta Administrador de secreto


La herramienta Administrador de secreto abstrae los detalles de implementación, como dónde y cómo se
almacenan los valores. Puede usar la herramienta sin conocer estos detalles de implementación. Los
valores se almacenan en un archivo de configuración de JSON en una carpeta de perfiles de usuarios
protegidos por el sistema en el equipo local:
Windows
macOS
Linux
Ruta de acceso de sistema de archivos:
%APPDATA%\Microsoft\UserSecrets\<user_secrets_id>\secrets.json

En los anteriores, las rutas de acceso de archivo, reemplace <user_secrets_id> con el UserSecretsId
valor especificado en el .csproj archivo.
No escriba código que depende de la ubicación o el formato de datos que se guardan con la herramienta
Administrador de secreto. Pueden cambiar estos detalles de implementación. Por ejemplo, los valores
secretos no se cifran, pero pudieron ser en el futuro.

Instalar la herramienta Administrador de secreto


La herramienta Administrador de secreto se incluye con la CLI de núcleo de .NET a partir de .NET Core
SDK 2.1.300. Para las versiones de .NET Core SDK anteriores 2.1.300, es necesaria la instalación de
herramientas.

TIP
Ejecute dotnet --version desde un shell de comandos para ver el número de versión de .NET Core SDK
instalado.

Se mostrará una advertencia si se usa .NET Core SDK incluye la herramienta:

The tool 'Microsoft.Extensions.SecretManager.Tools' is now included in the .NET Core SDK. Information
on resolving this warning is available at (https://aka.ms/dotnetclitools-in-box).

Instalar el Microsoft.Extensions.SecretManager.Tools paquete de NuGet en el proyecto de ASP.NET Core.


Por ejemplo:
[!code-xml]
Ejecute el siguiente comando en un shell de comandos para validar la instalación de la herramienta:

dotnet user-secrets -h

La herramienta Administrador de secreto muestra ejemplo de uso, opciones y la Ayuda de comando:


Usage: dotnet user-secrets [options] [command]

Options:
-?|-h|--help Show help information
--version Show version information
-v|--verbose Show verbose output
-p|--project <PROJECT> Path to project. Defaults to searching the current directory.
-c|--configuration <CONFIGURATION> The project configuration to use. Defaults to 'Debug'.
--id The user secret ID to use.

Commands:
clear Deletes all the application secrets
list Lists all the application secrets
remove Removes the specified user secret
set Sets the user secret to the specified value

Use "dotnet user-secrets [command] --help" for more information about a command.

NOTE
Debe estar en el mismo directorio que el .csproj archivo para ejecutar las herramientas que se definen en el .csproj
del archivo DotNetCliToolReference elementos.

Establecer un secreto
La herramienta Administrador de secreto opera en valores de configuración específicos del proyecto
almacenados en su perfil de usuario. Para utilizar secretos del usuario, definir una UserSecretsId
elemento dentro de un PropertyGroup de la .csproj archivo. El valor de UserSecretsId es arbitrario, pero
es único para el proyecto. Los desarrolladores suelen generan un GUID para el UserSecretsId .
[!code-xml]
[!code-xml]

TIP
En Visual Studio, haga clic en el proyecto en el Explorador de soluciones y seleccione administrar secretos del
usuario en el menú contextual. Este movimiento agrega un UserSecretsId elemento, que se rellena con un
GUID a la .csproj archivo. Visual Studio abre un secrets.json archivo en el editor de texto. Reemplace el contenido
de secrets.json con los pares clave-valor que se almacenará. Por ejemplo:
json { "Movies": { "ServiceApiKey": "12345", "ConnectionString": "Server=
(localdb)\\mssqllocaldb;Database=Movie-1;Trusted_Connection=True;MultipleActiveResultSets=true" }
}
.

Definir un secreto de la aplicación que consta de una clave y su valor. El secreto está asociado con el
proyecto UserSecretsId valor. Por ejemplo, ejecute el siguiente comando desde el directorio en el que el
.csproj archivo existe:

dotnet user-secrets set "Movies:ServiceApiKey" "12345"

En el ejemplo anterior, los dos puntos denota que Movies es un objeto literal con un ServiceApiKey
propiedad.
La herramienta Administrador de secreto puede utilizarse desde otros directorios demasiado. Use la
--project opción para proporcionar la ruta de acceso del sistema de archivos en el que el .csproj archivo
existe. Por ejemplo:

dotnet user-secrets set "Movies:ServiceApiKey" "12345" --project "C:\apps\WebApp1\src\WebApp1"

Establecer varios secretos


Un lote de secretos puede establecerse mediante la canalización de JSON a la set comando. En el
ejemplo siguiente, la input.json contenido del archivo se canaliza hacia el set comando.
Windows
macOS
Linux
Abra un shell de comandos y ejecute el siguiente comando:

type .\input.json | dotnet user-secrets set

Obtener acceso a un secreto


El API de configuración de ASP.NET Core proporciona acceso a los secretos de administrador de secreto.
Instalar el Microsoft.Extensions.Configuration.UserSecrets paquete NuGet.
Agregar el origen de configuración de usuario secretos con una llamada a AddUserSecrets en el Startup
constructor:
[!code-csharp]
El API de configuración de ASP.NET Core proporciona acceso a los secretos de administrador de secreto.
Si el proyecto tiene como destino .NET Framework, instale el
Microsoft.Extensions.Configuration.UserSecrets paquete NuGet.
En el núcleo de ASP.NET 2.0 o posterior, el origen de configuración de secretos de usuario se agrega
automáticamente en modo de desarrollo cuando el proyecto se llama CreateDefaultBuilder para
inicializar una nueva instancia del host con valores predeterminados preconfigurados.
CreateDefaultBuilder llamadas AddUserSecrets cuando el EnvironmentName es desarrollo:

[!code-csharp]
Cuando CreateDefaultBuilder no llama durante la construcción de host, agregue el origen de
configuración de usuario secretos con una llamada a AddUserSecrets en el Startup constructor:
[!code-csharp]
Secretos del usuario se pueden recuperar a través de la Configuration API:
[!code-csharp]
[!code-csharp]

Cadena de reemplazo con secretos


Almacenar contraseñas como texto sin formato es inseguro. Por ejemplo, una cadena de conexión de
base de datos se almacena en appSettings.JSON que se puede incluir una contraseña para el usuario
especificado:
{
"ConnectionStrings": {
"Movies": "Server=(localdb)\\mssqllocaldb;Database=Movie-1;User
Id=johndoe;Password=pass123;MultipleActiveResultSets=true"
}
}

Un enfoque más seguro consiste en almacenar la contraseña como un secreto. Por ejemplo:

dotnet user-secrets set "DbPassword" "pass123"

Quitar el Password par de clave y valor de la cadena de conexión en appSettings.JSON que se. Por
ejemplo:

{
"ConnectionStrings": {
"Movies": "Server=(localdb)\\mssqllocaldb;Database=Movie-1;User
Id=johndoe;MultipleActiveResultSets=true"
}
}

Se puede establecer el valor del secreto en un SqlConnectionStringBuilder del objeto contraseña


propiedad para completar la cadena de conexión:
[!code-csharp]
[!code-csharp]

Enumeración de los secretos


Se supone la aplicación secrets.json archivo contiene los siguientes dos secretos:

{
"Movies": {
"ServiceApiKey": "12345",
"ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=Movie-
1;Trusted_Connection=True;MultipleActiveResultSets=true"
}
}

Ejecute el siguiente comando desde el directorio en el que el .csproj archivo existe:

dotnet user-secrets list

Aparecerá el siguiente resultado:

Movies:ServiceApiKey = 12345
Movies:ConnectionString = Server=(localdb)\mssqllocaldb;Database=Movie-
1;Trusted_Connection=True;MultipleActiveResultSets=true

En el ejemplo anterior, un signo de dos puntos en los nombres de clave denota la jerarquía de objetos
dentro de secrets.json.

Quitar un secreto único


Se supone la aplicación secrets.json archivo contiene los siguientes dos secretos:

{
"Movies": {
"ServiceApiKey": "12345",
"ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=Movie-
1;Trusted_Connection=True;MultipleActiveResultSets=true"
}
}

Ejecute el siguiente comando desde el directorio en el que el .csproj archivo existe:

dotnet user-secrets remove "Movies:ConnectionString"

La aplicación secrets.json archivo se modificó para quitar el par de clave y valor asociado a la
MoviesConnectionString clave:

{
"Movies": {
"ServiceApiKey": "12345"
}
}

Ejecuta dotnet user-secrets list muestra el siguiente mensaje:

Movies:ServiceApiKey = 12345

Quitar todos los secretos


Se supone la aplicación secrets.json archivo contiene los siguientes dos secretos:

{
"Movies": {
"ServiceApiKey": "12345",
"ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=Movie-
1;Trusted_Connection=True;MultipleActiveResultSets=true"
}
}

Ejecute el siguiente comando desde el directorio en el que el .csproj archivo existe:

dotnet user-secrets clear

Se han eliminado todos los secretos de usuario de la aplicación de la secrets.json archivo:

{}

Ejecuta dotnet user-secrets list muestra el siguiente mensaje:

No secrets configured for this application.


Recursos adicionales
Configuración en ASP.NET Core
Proveedor de configuración de almacén de claves Azure en ASP.NET Core
Proveedor de configuración de almacén de claves
Azure en ASP.NET Core
22/06/2018 • 18 minutes to read • Edit Online

Por Luke Latham y Andrew Stanton-enfermera


ASP.NET Core 2.x
ASP.NET Core 1.x
Ver o descargar el código de ejemplo para 2.x:
Ejemplo básico (cómo descargar)-lee los valores de secreto en una aplicación.
Ejemplo de prefijo de nombre de clave (cómo descargar): valores secretos lecturas utilizando un prefijo de
nombre de la clave que representa la versión de una aplicación, lo que permite cargar un conjunto diferente
de valores secretos para cada versión de la aplicación.
Este documento explica cómo utilizar el Microsoft Azure Key Vault proveedor de configuración para cargar
valores de configuración de aplicación de secretos del almacén de claves de Azure. Almacén de claves de Azure
es un servicio basado en la nube que le ayuda a proteger las claves criptográficas y secretos usados por
aplicaciones y servicios. Escenarios comunes incluyen controlar el acceso a datos confidenciales de la
configuración y satisfacer el requisito para FIPS 140-2 nivel 2 validar módulos de seguridad de Hardware
(HSM ) al almacenar los datos de configuración. Esta característica está disponible para las aplicaciones que
tienen como destino ASP.NET Core 1.1 o posterior.

Package
Para usar el proveedor, agregue una referencia a la Microsoft.Extensions.Configuration.AzureKeyVault paquete.

Configuración de aplicación
Puede explorar el proveedor con el aplicaciones de ejemplo. Una vez que establezca un almacén de claves y
crear secretos en el almacén, las aplicaciones de ejemplo segura cargar los valores de secreto en sus
configuraciones y mostrarlos en las páginas Web.
El proveedor se agrega a la ConfigurationBuilder con el AddAzureKeyVault extensión. En las aplicaciones de
ejemplo, la extensión utiliza tres valores de configuración cargados desde el appSettings.JSON que se archivo.

CONFIGURACIÓN DE LA APLICACIÓN DESCRIPCIÓN EJEMPLO

Vault Nombre del almacén de claves de contosovault


Azure

ClientId Id. de aplicación de Azure Active 627e911e-43cc-61d4-992e-


Directory 12db9c81b413

ClientSecret Clave de aplicación de Azure Active g58K3dtg59o1Pa+e59v2Tx829w6VxTB


Directory 2yv9sv/101di=
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((context, config) =>
{
var builtConfig = config.Build();

var keyVaultConfigBuilder = new ConfigurationBuilder();

keyVaultConfigBuilder.AddAzureKeyVault(
$"https://{builtConfig["Vault"]}.vault.azure.net/",
builtConfig["ClientId"],
builtConfig["ClientSecret"]);

var keyVaultConfig = keyVaultConfigBuilder.Build();

config.AddConfiguration(keyVaultConfig);
})
.UseStartup<Startup>();

Creación de secretos del almacén de claves y cargar los valores de


configuración (ejemplo de basic)
1. Crear un almacén de claves y configurar Azure Active Directory (Azure AD ) para la aplicación siguiendo
las instrucciones de empezar a trabajar con el almacén de claves de Azure.
Agregar secretos en el almacén de claves mediante el módulo de PowerShell de almacén de claves
AzureRM disponibles desde el Galería de PowerShell, el API de REST del almacén de claves de Azure,
o la Portal de azure. Los secretos se crean como Manual o certificado secretos. Certificado secretos
están certificados para su uso por aplicaciones y servicios, pero no son compatibles con el proveedor
de configuración. Debe utilizar el Manual opción para crear los secretos de par nombre / valor para su
uso con el proveedor de configuración.
Secretos simples se crean como pares nombre / valor. Nombres de secreto de almacén de
claves Azure están limitados a caracteres alfanuméricos y guiones.
Usan valores jerárquicos (secciones de configuración) -- (dos guiones) como separador en el
ejemplo. Caracteres de dos puntos, que normalmente se utilizan para delimitar una sección de
una subclave en configuración de ASP.NET Core, no están permitidos en los nombres de
secreto. Por lo tanto, se usa dos guiones y se intercambian en dos puntos cuando se cargan los
secretos en la configuración de la aplicación.
Cree dos Manual secretos con los siguientes pares de nombre-valor. El secreto del primer es un
nombre simple y el valor y el secreto del segundo crea un valor secreto con una sección y la
subclave en el nombre de secreto:
SecretName : secret_value_1
Section--SecretName : secret_value_2
Registrar la aplicación de ejemplo con Azure Active Directory.
Autorizar la aplicación para tener acceso al almacén de claves. Cuando se usa el
Set-AzureRmKeyVaultAccessPolicy cmdlet de PowerShell para autorizar la aplicación para tener acceso
al almacén de claves, proporcionar List y Get acceso a los secretos con
-PermissionsToSecrets list,get .
2. Actualización de la aplicación appSettings.JSON que se archivo con los valores de Vault , ClientId , y
ClientSecret .

3. Ejecute la aplicación de ejemplo, que obtiene sus valores de configuración de IConfigurationRoot con el
mismo nombre que el nombre de secreto.
Valores no son jerárquicos: el valor de SecretName se obtiene con config["SecretName"] .
Valores jerárquicos (secciones): Use : notación (coma) o el GetSection método de extensión. Use
cualquiera de estos métodos para obtener el valor de configuración:
config["Section:SecretName"]
config.GetSection("Section")["SecretName"]

Cuando se ejecuta la aplicación, una página Web muestra los valores cargados secretos:

Crear el almacén de claves con prefijo secretos y cargar los valores de


configuración (clave-nombre-ejemplo de prefijo)
AddAzureKeyVault También proporciona una sobrecarga que acepta una implementación de
IKeyVaultSecretManager , que le permite controlar cómo clave secretos del almacén se convierten en las claves de
configuración. Por ejemplo, puede implementar la interfaz para cargar valores secretos basándose en un valor
de prefijo que proporcione al iniciar la aplicación. Esto le permite, por ejemplo, para cargar los secretos en
función de la versión de la aplicación.

WARNING
No utilizar prefijos en secretos del almacén de claves colocar secretos para varias aplicaciones en el mismo almacén de
claves o colocar secretos entorno (por ejemplo, desarrollo frente a producción secretos) en el mismo almacén. Se
recomienda que diferentes aplicaciones y entornos de desarrollo o de producción usan almacenes claves independientes
para aislar los entornos de aplicación para el nivel más alto de seguridad.

Con la segunda aplicación de ejemplo, crear un secreto en el almacén de claves para 5000-AppSecret (períodos
no están permitidos en los nombres de secreto de almacén de claves) que representa un secreto de la aplicación
para la versión 5.0.0.0 de la aplicación. Para obtener otra versión, 5.1.0.0, crear un secreto para 5100-AppSecret .
Cada versión de la aplicación carga su propio valor secreto en su configuración como AppSecret , extracción
desactivar la versión cuando se cargue el secreto. Implementación del ejemplo se muestra a continuación:
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((context, config) =>
{
// The appVersion obtains the app version (5.0.0.0), which
// is set in the project file and obtained from the entry
// assembly. The versionPrefix holds the version without
// dot notation for the PrefixKeyVaultSecretManager.
var appVersion = Assembly.GetEntryAssembly().GetName().Version.ToString();
var versionPrefix = appVersion.Replace(".", string.Empty);

var builtConfig = config.Build();

var keyVaultConfigBuilder = new ConfigurationBuilder();

keyVaultConfigBuilder.AddAzureKeyVault(
$"https://{builtConfig["Vault"]}.vault.azure.net/",
builtConfig["ClientId"],
builtConfig["ClientSecret"],
new PrefixKeyVaultSecretManager(versionPrefix));

var keyVaultConfig = keyVaultConfigBuilder.Build();

config.AddConfiguration(keyVaultConfig);
})
.UseStartup<Startup>();

public class PrefixKeyVaultSecretManager : IKeyVaultSecretManager


{
private readonly string _prefix;

public PrefixKeyVaultSecretManager(string prefix)


{
_prefix = $"{prefix}-";
}

public bool Load(SecretItem secret)


{
// Load a vault secret when its secret name starts with the
// prefix. Other secrets won't be loaded.
return secret.Identifier.Name.StartsWith(_prefix);
}

public string GetKey(SecretBundle secret)


{
// Remove the prefix from the secret name and replace two
// dashes in any name with the KeyDelimiter, which is the
// delimiter used in configuration (usually a colon). Azure
// Key Vault doesn't allow a colon in secret names.
return secret.SecretIdentifier.Name
.Substring(_prefix.Length)
.Replace("--", ConfigurationPath.KeyDelimiter);
}
}

El Load método se llama mediante un algoritmo de proveedor que recorre en iteración los secretos del almacén
para buscar los que tienen el prefijo de la versión. Cuando se encuentra un prefijo de versión con Load , el
algoritmo utiliza el GetKey método para devolver el nombre de configuración del nombre del secreto. Quita el
prefijo de la versión del nombre del secreto y devuelve el resto del nombre del secreto para cargar en la
configuración de la aplicación de pares nombre / valor.
Al implementar este enfoque:
1. Se cargan los secretos del almacén de claves.
2. El secreto de cadena para 5000-AppSecret se encuentran coincidencias.
3. La versión 5000 (con el guión), se quitan el nombre de clave dejando AppSecret para cargar con el valor de
secreto en configuración de la aplicación.

NOTE
También puede proporcionar sus propios KeyVaultClient implementación AddAzureKeyVault . Proporciona a un cliente
personalizado le permite compartir una única instancia del cliente entre el proveedor de configuración y otras partes de la
aplicación.

1. Crear un almacén de claves y configurar Azure Active Directory (Azure AD ) para la aplicación siguiendo
las instrucciones de empezar a trabajar con el almacén de claves de Azure.
Agregar secretos en el almacén de claves mediante el módulo de PowerShell de almacén de claves
AzureRM disponibles desde el Galería de PowerShell, el API de REST del almacén de claves de Azure,
o la Portal de azure. Los secretos se crean como Manual o certificado secretos. Certificado secretos
están certificados para su uso por aplicaciones y servicios, pero no son compatibles con el proveedor
de configuración. Debe utilizar el Manual opción para crear los secretos de par nombre / valor para su
uso con el proveedor de configuración.
Usan valores jerárquicos (secciones de configuración) -- (dos guiones) como separador.
Cree dos Manual secretos con los siguientes pares de nombre y valor:
5000-AppSecret : 5.0.0.0_secret_value
5100-AppSecret : 5.1.0.0_secret_value
Registrar la aplicación de ejemplo con Azure Active Directory.
Autorizar la aplicación para tener acceso al almacén de claves. Cuando se usa el
Set-AzureRmKeyVaultAccessPolicy cmdlet de PowerShell para autorizar la aplicación para tener acceso
al almacén de claves, proporcionar List y Get acceso a los secretos con
-PermissionsToSecrets list,get .
2. Actualización de la aplicación appSettings.JSON que se archivo con los valores de Vault , ClientId , y
ClientSecret .

3. Ejecute la aplicación de ejemplo, que obtiene sus valores de configuración de IConfigurationRoot con el
mismo nombre que el nombre de secreto con prefijo. En este ejemplo, el prefijo es la versión de la
aplicación, que proporciona a los PrefixKeyVaultSecretManager cuando agrega el proveedor de
configuración de almacén de claves de Azure. El valor de AppSecret se obtiene con config["AppSecret"] .
La página Web generada por la aplicación muestra el valor cargado:

4. Cambiar la versión del ensamblado en el archivo de proyecto de aplicación 5.0.0.0 a 5.1.0.0 y vuelva a
ejecutar la aplicación. En esta ocasión, el valor de secreto devuelto es 5.1.0.0_secret_value . La página
Web generada por la aplicación muestra el valor cargado:
Controlar el acceso a la ClientSecret
Use la herramienta Administrador de secreto para mantener la ClientSecret fuera de su árbol de código fuente
del proyecto. Con el Administrador de secreto, para asocia los secretos de aplicación a un proyecto específico y
compartirlos en varios proyectos.
Al desarrollar una aplicación de .NET Framework en un entorno que admite certificados, puede autenticarse en
el almacén de claves de Azure con un certificado X.509. Clave privada del certificado X.509 es administrada por
el sistema operativo. Para obtener más información, consulte autenticar con un certificado en lugar de un
secreto de cliente. Use la AddAzureKeyVault sobrecarga que acepta un X509Certificate2 .

var store = new X509Store(StoreLocation.CurrentUser);


store.Open(OpenFlags.ReadOnly);
var cert = store.Certificates.Find(X509FindType.FindByThumbprint, config["CertificateThumbprint"], false);

builder.AddAzureKeyVault(
config["Vault"],
config["ClientId"],
cert.OfType<X509Certificate2>().Single(),
new EnvironmentSecretManager(env.ApplicationName));
store.Close();

Configuration = builder.Build();

Volver a cargar secretos


Los secretos se almacenan en caché hasta que IConfigurationRoot.Reload() se llama. Caducado, deshabilitado, y
por la aplicación hasta que no se respeten la secretos actualizados en el almacén de claves Reload se ejecuta.

Configuration.Reload();

Secretos deshabilitados y expirados


Secretos deshabilitados y expirados producen un KeyVaultClientException . Para evitar que la aplicación genere,
reemplace la aplicación o actualizar el secreto deshabilitado o expirado.

Solución de problemas
Cuando no se puede cargar la configuración mediante el proveedor de la aplicación, se escribe un mensaje de
error en la infraestructura del registro de ASP.NET. Las siguientes condiciones se evita que la configuración de
carga:
La aplicación no está configurada correctamente en Azure Active Directory.
El almacén de claves no existe en el almacén de claves de Azure.
La aplicación no está autorizada para tener acceso al almacén de claves.
No incluye la directiva de acceso Get y List permisos.
En el almacén de claves, los datos de configuración (par nombre / valor) se denominó incorrectamente, falta,
deshabilitado o expirado.
La aplicación tiene el nombre de almacén de claves incorrecto ( Vault ), Id. de aplicación de Azure AD (
ClientId ), o la clave de AD de Azure ( ClientSecret ).
La clave de AD de Azure ( ClientSecret ) ha expirado.
La clave de configuración (nombre) es incorrecta en la aplicación para el valor que está intentando cargar.

Recursos adicionales
Configuración
Microsoft Azure: Almacén de claves
Microsoft Azure: Documentación de almacén de claves
Cómo generar y transferir protegidas con HSM de claves para el almacén de claves de Azure
Clase KeyVaultClient
Ataques de evitar Cross-Site falsificación de
solicitud (XSRF/CSRF) en ASP.NET Core
22/06/2018 • 26 minutes to read • Edit Online

Por Steve Smith, Fiyaz Hasan, y Rick Anderson


Falsificación de solicitudes entre sitios (también conocido como XSRF o CSRF, pronunciado vea
navegación) es un ataque contra mediante el cual una aplicación web malintencionado puede influir
en la interacción entre un explorador del cliente y una aplicación web que confía en que las
aplicaciones hospedadas en web Explorador. Estos ataques son posibles porque los exploradores
web envían algunos tipos de tokens de autenticación automáticamente con cada solicitud a un sitio
Web. Esta forma de vulnerabilidad de seguridad es también conocido como un ataque de un solo clic
o sesión conducir porque el ataque aprovecha las ventajas de sesión del autentica previamente en el
usuario.
Un ejemplo de un ataque CSRF:
1. Un usuario inicia sesión en www.good-banking-site.com utilizando la autenticación de
formularios. El servidor autentica al usuario y emite una respuesta que incluye una cookie de
autenticación. El sitio es vulnerable a ataques porque confía en cualquier solicitud recibida con
una cookie de autenticación válida.
2. El usuario visita un sitio malintencionado, www.bad-crook-site.com .
El sitio malintencionado, www.bad-crook-site.com , contiene un formulario HTML similar al
siguiente:

<h1>Congratulations! You're a Winner!</h1>


<form action="http://good-banking-site.com/api/account" method="post">
<input type="hidden" name="Transaction" value="withdraw">
<input type="hidden" name="Amount" value="1000000">
<input type="submit" value="Click to collect your prize!">
</form>

Tenga en cuenta que el formulario action entradas para el sitio sea vulnerable, no para el
sitio malintencionado. Esta es la parte "cross-site" de CSRF.
3. El usuario selecciona el botón Enviar. El explorador realiza la solicitud y automáticamente
incluye la cookie de autenticación para el dominio solicitado, www.good-banking-site.com .
4. La solicitud se ejecuta en el www.good-banking-site.com servidor con contexto de autenticación
del usuario y pueden realizar cualquier acción que puede realizar un usuario autenticado.
Además el escenario donde el usuario selecciona el botón para enviar el formulario, el sitio
malintencionado podría:
Ejecutar un script que envía automáticamente el formulario.
Envía el envío del formulario como una solicitud AJAX.
Ocultar el formulario de uso de CSS.
Estos escenarios alternativos no requieren ninguna acción o entrada del usuario que no sea
inicialmente visitar el sitio malintencionado.
Uso de HTTPS no impide que un ataque CSRF. El sitio malintencionado puede enviar un
https://www.good-banking-site.com/ solicitar de manera tan sencilla como puede enviar una solicitud
insegura.
Algunos ataques de destino que respondan a las solicitudes GET, en cuyo caso se puede utilizar una
etiqueta de imagen para realizar la acción. Esta forma de ataque es habitual en los sitios de foro que
permiten imágenes pero bloquean JavaScript. Las aplicaciones que cambian el estado de las
solicitudes GET, donde se modifican las variables o los recursos, son vulnerables a ataques
malintencionados. Las solicitudes GET que cambiarán el estado no son seguros. Una práctica
recomendada consiste en no cambiar nunca el estado en una solicitud GET.
Los ataques CSRF son posibles con aplicaciones web que usan cookies para la autenticación porque:
Los exploradores almacenan las cookies emitidas por una aplicación web.
Almacenado cookies incluyen cookies de sesión para los usuarios autenticados.
Exploradores envían que todas las cookies asociadas con un dominio a la aplicación web de todas
las solicitudes, independientemente de cómo se generó la solicitud de aplicación dentro del
explorador.
Sin embargo, no están limitados CSRF ataques para aprovecharse de las cookies. Por ejemplo, la
autenticación básica e implícita también son vulnerables. Una vez que un usuario inicia sesión con
autenticación básica o implícita, el explorador envía automáticamente las credenciales hasta que la
sesión† finaliza.
†En este contexto, sesión hace referencia a la sesión de cliente durante el cual el usuario está
autenticado. Es no relacionados con las sesiones de servidor o Middleware de sesión de ASP.NET
Core.
Los usuarios pueden protegerse contra vulnerabilidades CSRF, tome precauciones:
Inicie sesión en las aplicaciones web cuando termine de utilizarlos.
Borrar las cookies del explorador periódicamente.
Sin embargo, las vulnerabilidades CSRF son fundamentalmente un problema con la aplicación web,
no el usuario final.

Conceptos básicos de autenticación


Autenticación basada en cookies es una forma popular de autenticación. Sistemas de autenticación
basada en token son cada vez más popularidad, especialmente para las aplicaciones de página única
(SPAs).
Autenticación basada en cookies
Cuando un usuario se autentica con su nombre de usuario y contraseña, le emite un token, que
contiene un vale de autenticación que se puede usar para la autenticación y autorización. El token se
almacena como hace que una cookie que acompaña a cada solicitud del cliente. Generar y validar
esta cookie se realizan mediante el Middleware de autenticación de la Cookie. El middleware serializa
una entidad de seguridad de usuario en una cookie cifrada. En solicitudes posteriores, el middleware
valida la cookie, vuelve a crear la entidad de seguridad y asigna la entidad de seguridad para la
usuario propiedad de HttpContext.
Autenticación basada en token
Cuando un usuario se autentica, le emite un token (no un token antiforgery). El token contiene
información de usuario en forma de notificaciones o un símbolo (token) de referencia que señala la
aplicación al estado de usuario que se mantienen en la aplicación. Cuando un usuario intenta acceder
a un recurso que requiere autenticación, el token se envía a la aplicación con un encabezado de
autorización adicionales en forma de token de portador. Esto hace que la aplicación sin estado. En
cada solicitud posterior, el token se pasa en la solicitud para la validación del lado servidor. Este token
no es cifrados; tiene codificado. En el servidor, se descodifica el token para acceder a su información.
Para enviar el token en las solicitudes subsiguientes, almacene el token en el almacenamiento local
del explorador. No se preocupe acerca de vulnerabilidad CSRF si el token se almacena en
almacenamiento local del explorador. CSRF es una preocupación cuando el token se almacena en
una cookie.
Varias aplicaciones hospedadas en un dominio
Entornos de hospedaje compartidos son vulnerables a secuestro de sesión, inicio de sesión CSRF y
otros ataques.
Aunque example1.contoso.net y example2.contoso.net son diferentes de los hosts, hay una relación
de confianza implícita entre los hosts bajo la *.contoso.net dominio. Esta relación de confianza
implícita permite a los hosts no sea de confianza influir en sus respectivas cookies (las directivas de
mismo origen que rigen las solicitudes AJAX necesariamente no se aplican a las cookies HTTP ).
Se pueden evitar ataques que aprovechan las cookies de confianza entre las aplicaciones hospedadas
en el mismo dominio, como no compartir dominios. Cuando cada aplicación se hospeda en su
propio dominio, no hay ninguna relación de confianza implícita cookie aprovechar.

Configuración de ASP.NET Core antiforgery


WARNING
ASP.NET Core implementa con antiforgery protección de datos de ASP.NET Core. La pila de protección de
datos debe configurarse para que funcione en una granja de servidores. Vea configurando la protección de
datos para obtener más información.

En el núcleo de ASP.NET 2.0 o posterior, el FormTagHelper inyecta antiforgery tokens en elementos


de formulario HTML. El siguiente marcado en un archivo Razor automáticamente genera tokens
antiforgery:

<form method="post">
...
</form>

De forma similar, IHtmlHelper.BeginForm genera antiforgery símbolos (tokens) de forma


predeterminada si el método del formulario no es GET.
La generación automática de símbolos (tokens) antiforgery para los elementos de formulario HTML
se produce cuando el <form> etiqueta contiene el method="post" atributo y cualquiera de las
siguientes son verdaderas:
El atributo de acción está vacío ( action="" ).
No se especifica el atributo de acción ( <form method="post"> ).

Se puede deshabilitar la generación automática de símbolos (tokens) antiforgery para los elementos
de formulario HTML:
Deshabilitar explícitamente antiforgery tokens con el asp-antiforgery atributo:
<form method="post" asp-antiforgery="false">
...
</form>

El elemento de formulario es darse de alta en horizontal de aplicaciones auxiliares de


etiquetas mediante el uso de la aplicación auxiliar de etiqueta ! símbolos de desactivación:

<!form method="post">
...
</!form>

Quitar el FormTagHelper de la vista. La FormTagHelper puede quitarse desde una vista


agregando la siguiente directiva a la vista Razor:

@removeTagHelper Microsoft.AspNetCore.Mvc.TagHelpers.FormTagHelper,
Microsoft.AspNetCore.Mvc.TagHelpers

NOTE
Las páginas de Razor están protegidos automáticamente frente a XSRF/CSRF. Para obtener más información,
consulte XSRF/CSRF y páginas de Razor.

El enfoque más común para defenderse contra ataques CSRF consiste en usar la patrón del Token
Sincronizador (STP ). STP se utiliza cuando el usuario solicita una página con datos del formulario:
1. El servidor envía un token asociado con la identidad del usuario actual al cliente.
2. El cliente devuelve el token en el servidor para la comprobación.
3. Si el servidor recibe un token que no coincide con la identidad del usuario autenticado, se rechaza
la solicitud.
El token es único y no previstos. El token puede usarse también para asegurarse de la secuencia
correcta de una serie de solicitudes (por ejemplo, garantizar la secuencia de solicitud de: página 1 –
página 2 – página 3). Todos los formularios en plantillas de MVC de ASP.NET Core y páginas de
Razor generan tokens antiforgery. El siguiente par de ejemplos de vista genera tokens antiforgery:

<form asp-controller="Manage" asp-action="ChangePassword" method="post">


...
</form>

@using (Html.BeginForm("ChangePassword", "Manage"))


{
...
}

Agregar explícitamente un token antiforgery a una <form> elemento sin usar aplicaciones auxiliares
de etiquetas con la aplicación auxiliar HTML @Html.AntiForgeryToken :

<form action="/" method="post">


@Html.AntiForgeryToken()
</form>

En cada uno de los casos anteriores, ASP.NET Core agrega un campo de formulario oculto similar al
siguiente:
<input name="__RequestVerificationToken" type="hidden" value="CfDJ8NrAkS ... s2-m9Yw">

ASP.NET Core incluye tres filtros para trabajar con tokens antiforgery:
ValidateAntiForgeryToken
AutoValidateAntiforgeryToken
IgnoreAntiforgeryToken

Opciones de antiforgery
Personalizar opciones antiforgery en Startup.ConfigureServices :

services.AddAntiforgery(options =>
{
options.CookieDomain = "contoso.com";
options.CookieName = "X-CSRF-TOKEN-COOKIENAME";
options.CookiePath = "Path";
options.FormFieldName = "AntiforgeryFieldname";
options.HeaderName = "X-CSRF-TOKEN-HEADERNAME";
options.RequireSsl = false;
options.SuppressXFrameOptionsHeader = false;
});

OPCIÓN DESCRIPCIÓN

Cookie Determina la configuración utilizada para crear la


cookie antiforgery.

CookieDomain El dominio de la cookie. Tiene como valor


predeterminado null . Esta propiedad está obsoleta
y se quitará en una versión futura. La alternativa
recomendada es Cookie.Domain.

CookieName El nombre de la cookie. Si no está establecida, el


sistema genera un nombre único que comienza con la
DefaultCookiePrefix (". AspNetCore.Antiforgery."). Esta
propiedad está obsoleta y se quitará en una versión
futura. La alternativa recomendada es Cookie.Name.

CookiePath La ruta de acceso establecido en la cookie. Esta


propiedad está obsoleta y se quitará en una versión
futura. La alternativa recomendada es Cookie.Path.

FormFieldName El nombre del campo oculto del formulario utilizado


por el sistema antiforgery para representar antiforgery
tokens en las vistas.

HeaderName El nombre del encabezado utilizado por el sistema


antiforgery. Si null , el sistema considera que solo los
datos de formulario.
OPCIÓN DESCRIPCIÓN

RequireSsl Especifica si se requiere SSL por el sistema antiforgery.


Si true , se producirá un error en las solicitudes sin
SSL. Tiene como valor predeterminado false . Esta
propiedad está obsoleta y se quitará en una versión
futura. La alternativa recomendada es establecer
Cookie.SecurePolicy.

SuppressXFrameOptionsHeader Especifica si se debe suprimir la generación de la


X-Frame-Options encabezado. De forma
predeterminada, el encabezado se genera con un valor
de "SAMEORIGIN". Tiene como valor predeterminado
false .

Para obtener más información, consulte CookieAuthenticationOptions.

Configurar características antiforgery con IAntiforgery


IAntiforgery proporciona la API para configurar características antiforgery. IAntiforgery se puede
solicitar en el Configure método de la Startup clase. En el ejemplo siguiente se usa el middleware
de página principal de la aplicación para generar un token de antiforgery y enviarlo en la respuesta
como una cookie (mediante la convención de nomenclatura de manera predeterminada Angular que
se describe más adelante en este tema):

public void Configure(IApplicationBuilder app, IAntiforgery antiforgery)


{
app.Use(next => context =>
{
string path = context.Request.Path.Value;

if (
string.Equals(path, "/", StringComparison.OrdinalIgnoreCase) ||
string.Equals(path, "/index.html", StringComparison.OrdinalIgnoreCase))
{
// The request token can be sent as a JavaScript-readable cookie,
// and Angular uses it by default.
var tokens = antiforgery.GetAndStoreTokens(context);
context.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken,
new CookieOptions() { HttpOnly = false });
}

return next(context);
});
}

Requerir la validación antiforgery


ValidateAntiForgeryToken es un filtro de acción que se puede aplicar a una acción individual, un
controlador, o de forma global. Las solicitudes realizadas a las acciones que se ha aplicado este filtro
se bloquean a menos que la solicitud incluye un token de antiforgery válido.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> RemoveLogin(RemoveLoginViewModel account)
{
ManageMessageId? message = ManageMessageId.Error;
var user = await GetCurrentUserAsync();

if (user != null)
{
var result =
await _userManager.RemoveLoginAsync(
user, account.LoginProvider, account.ProviderKey);

if (result.Succeeded)
{
await _signInManager.SignInAsync(user, isPersistent: false);
message = ManageMessageId.RemoveLoginSuccess;
}
}

return RedirectToAction(nameof(ManageLogins), new { Message = message });


}

El ValidateAntiForgeryToken atributo requiere un token para las solicitudes a los métodos de acción
que decora, incluidas las solicitudes HTTP GET. Si el ValidateAntiForgeryToken atributo se aplica en
todos los controladores de la aplicación, puede reemplazarse por el IgnoreAntiforgeryToken atributo.

NOTE
ASP.NET Core no admite la adición de tokens antiforgery a solicitudes GET automáticamente.

Validar automáticamente los tokens antiforgery para solo los métodos HTTP no seguros
Las aplicaciones ASP.NET Core no generan antiforgery tokens para los métodos HTTP seguros (GET,
HEAD, opciones y seguimiento). En lugar de aplicar ampliamente el ValidateAntiForgeryToken
atributo y, a continuación, reemplazar con IgnoreAntiforgeryToken atributos, los
AutoValidateAntiforgeryToken atributo se puede usar. Este atributo funciona de forma idéntica a la
ValidateAntiForgeryToken de atributo, salvo que no requiere tokens para las solicitudes realizadas
con los siguientes métodos HTTP:
GET
HEAD
OPCIONES
TRACE
Se recomienda usar AutoValidateAntiforgeryToken ampliamente para escenarios de API no. Esto
garantiza que las acciones de entrada están protegidas de forma predeterminada. La alternativa es
omitir antiforgery símbolos (tokens) de forma predeterminada, a menos que
ValidateAntiForgeryToken se aplica a los métodos de acción individuales. No esté más probable es
que en este escenario para un método de acción POST que desee dejar protegida por error, salir de
la aplicación sea vulnerable a ataques CSRF. Todas las publicaciones deben enviar el token
antiforgery.
Las API no tienen un mecanismo automático para el envío de la parte no cookie del token. La
implementación probablemente depende de la implementación del código de cliente. A continuación
se muestran algunos ejemplos:
Ejemplo de nivel de clase:
[Authorize]
[AutoValidateAntiforgeryToken]
public class ManageController : Controller
{

Ejemplo global:

services.AddMvc(options =>
options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()));

Reemplazo global o atributos antiforgery de controlador


El IgnoreAntiforgeryToken filtro se utiliza para eliminar la necesidad de un token antiforgery para
una acción determinada (o controlador). Cuando se aplica, invalida este filtro
ValidateAntiForgeryToken y AutoValidateAntiforgeryToken filtros especificados en un nivel más alto
(global o en un controlador).

[Authorize]
[AutoValidateAntiforgeryToken]
public class ManageController : Controller
{
[HttpPost]
[IgnoreAntiforgeryToken]
public async Task<IActionResult> DoSomethingSafe(SomeViewModel model)
{
// no antiforgery token required
}
}

Tokens de actualización después de la autenticación


Símbolos (tokens) se debe actualizar después de que el usuario se autenticó mediante redirige al
usuario a una vista o página de las páginas de Razor.

JavaScript, AJAX y SPAs


En las aplicaciones tradicionales basadas en HTML, antiforgery símbolos (tokens) se pasa al servidor
mediante campos ocultos de formulario. En las aplicaciones modernas basadas en JavaScript y SPAs,
muchas de las solicitudes se realizan mediante programación. Estas solicitudes de AJAX pueden usar
otras técnicas (por ejemplo, los encabezados de solicitud o cookies) para enviar el token.
Si se usan cookies para almacenar los tokens de autenticación y para autenticar las solicitudes de API
en el servidor, CSRF es un posible problema. Si se usa almacenamiento local para almacenar el
token, podría ser mitigada vulnerabilidad CSRF porque valores desde el almacenamiento local no se
envían automáticamente al servidor con cada solicitud. Por lo tanto, puede utilizar almacenamiento
local para almacenar el token antiforgery en el cliente y enviar el token como un encabezado de
solicitud es un enfoque recomendado.
JavaScript
Uso de JavaScript con vistas, el token de puede crearse con un servicio desde dentro de la vista.
Insertar el Microsoft.AspNetCore.Antiforgery.IAntiforgery servicio en la vista y llame a
GetAndStoreTokens:
@{
ViewData["Title"] = "AJAX Demo";
}
@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Xsrf
@functions{
public string GetAntiXsrfRequestToken()
{
return Xsrf.GetAndStoreTokens(Context).RequestToken;
}
}

<input type="hidden" id="RequestVerificationToken"


name="RequestVerificationToken" value="@GetAntiXsrfRequestToken()">

<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>

<div class="row">
<p><input type="button" id="antiforgery" value="Antiforgery"></p>
<script>
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (xhttp.readyState == XMLHttpRequest.DONE) {
if (xhttp.status == 200) {
alert(xhttp.responseText);
} else {
alert('There was an error processing the AJAX request.');
}
}
};

document.addEventListener('DOMContentLoaded', function() {
document.getElementById("antiforgery").onclick = function () {
xhttp.open('POST', '@Url.Action("Antiforgery", "Home")', true);
xhttp.setRequestHeader("RequestVerificationToken",
document.getElementById('RequestVerificationToken').value);
xhttp.send();
}
});
</script>
</div>

Este enfoque elimina la necesidad de tratar directamente con la configuración de cookies del
servidor o para leerlos desde el cliente.
El ejemplo anterior utiliza JavaScript para leer el valor del campo oculto para el encabezado de
POST de AJAX.
JavaScript puede tokens de acceso en las cookies y usar el contenido de la cookie para crear un
encabezado con el valor del token.

context.Response.Cookies.Append("CSRF-TOKEN", tokens.RequestToken,
new Microsoft.AspNetCore.Http.CookieOptions { HttpOnly = false });

Suponiendo que la secuencia de comandos las solicitudes para enviar el token en un encabezado
denominado X-CSRF-TOKEN , configurar el servicio antiforgery para buscar la X-CSRF-TOKEN
encabezado:

services.AddAntiforgery(options => options.HeaderName = "X-CSRF-TOKEN");

En el ejemplo siguiente se utiliza JavaScript para realizar una solicitud de AJAX con el encabezado
adecuado:

function getCookie(cname) {
var name = cname + "=";
var decodedCookie = decodeURIComponent(document.cookie);
var ca = decodedCookie.split(';');
for(var i = 0; i <ca.length; i++) {
var c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return "";
}

var csrfToken = getCookie("CSRF-TOKEN");

var xhttp = new XMLHttpRequest();


xhttp.onreadystatechange = function() {
if (xhttp.readyState == XMLHttpRequest.DONE) {
if (xhttp.status == 200) {
alert(xhttp.responseText);
} else {
alert('There was an error processing the AJAX request.');
}
}
};
xhttp.open('POST', '/api/password/changepassword', true);
xhttp.setRequestHeader("Content-type", "application/json");
xhttp.setRequestHeader("X-CSRF-TOKEN", csrfToken);
xhttp.send(JSON.stringify({ "newPassword": "ReallySecurePassword999$$$" }));

AngularJS
AngularJS usa una convención para dirección CSRF. Si el servidor envía una cookie con el nombre
XSRF-TOKEN , AngularJS $http servicio agrega el valor de la cookie a un encabezado cuando envía
una solicitud al servidor. Este proceso es automático. El encabezado no tiene que establecerse de
forma explícita. El nombre de encabezado es X-XSRF-TOKEN . El servidor debe detectar este
encabezado y validar su contenido.
Para la API de ASP.NET básica trabajar con esta convención:
Configurar la aplicación para proporcionar un token en una cookie denominada XSRF-TOKEN .
Configurar el servicio para que busque un encabezado denominado antiforgery X-XSRF-TOKEN .

services.AddAntiforgery(options => options.HeaderName = "X-XSRF-TOKEN");

Vea o descargue el código de ejemplo (cómo descargarlo)

Extender antiforgery
El IAntiForgeryAdditionalDataProvider tipo permite a los desarrolladores extender el
comportamiento del sistema anti-CSRF por datos adicionales de ida y vuelta de cada token. El
GetAdditionalData método se llama cada vez que se genera un token de campo y el valor devuelto
está incrustado en el token generado. Un implementador podría devolver una marca de tiempo, un
nonce o cualquier otro valor y, a continuación, llame a ValidateAdditionalData para validar estos
datos cuando se valida el token. Nombre de usuario del cliente ya está incrustada en los tokens
generados, por lo que no es necesario incluir esta información. Si un token incluye datos
suplementarios pero no IAntiForgeryAdditionalDataProvider está configurado, no validados los
datos suplementarios.

Recursos adicionales
CSRF en Abrir proyecto de seguridad de aplicación Web (OWASP ).
Evitar los ataques de redirección abierta en ASP.NET
Core
22/06/2018 • 6 minutes to read • Edit Online

Una aplicación web que redirija a una dirección URL que se especifica a través de la solicitud como la cadena de
consulta o un formulario de datos potencialmente puede alterada para redirigir a los usuarios a una dirección URL
externa, malintencionada. Esta modificación se llama a un ataque de redirección abierta.
Cada vez que la lógica de aplicación se redirige a una dirección URL especificada, debe comprobar que la dirección
URL de redireccionamiento no se ha manipulado. ASP.NET Core tiene funcionalidad integrada para ayudar a
proteger las aplicaciones frente a ataques de redirección abierta (también conocido como abrir redirección).

¿Qué es un ataque de redirección abierta?


Las aplicaciones Web con frecuencia redirección a los usuarios a una página de inicio de sesión cuando accedan a
los recursos que requieren autenticación. La redirección typlically incluye un returnUrl parámetro de cadena de
consulta para que el usuario puede devolverse a la dirección URL solicitada originalmente después de que han
iniciado sesión correctamente. Después de que el usuario se autentica, se le redirige a la dirección URL que tenían
originalmente solicitada.
Dado que la dirección URL de destino se especifica en la cadena de consulta de la solicitud, un usuario
malintencionado podría manipular la cadena de consulta. Una cadena de consulta modificada podría permitir al
sitio redirigir al usuario a un sitio externo, malintencionado. Esta técnica se denomina un ataque de redirección (o
redirección) abierto.
Un ataque de ejemplo
Un usuario malintencionado podría desarrollar un ataque diseñado para permitir el acceso de usuario
malintencionado para las credenciales de un usuario o información confidencial en la aplicación. Para iniciar el
ataque, convencer a los usuarios hacer clic en un vínculo a la página de inicio de sesión de su sitio, con un
returnUrl valor cadena de consulta que se agrega a la dirección URL. Por ejemplo, el NerdDinner.com aplicación
de ejemplo (escrito para ASP.NET MVC ) incluye aquí tal una página de inicio de sesión:
http://nerddinner.com/Account/LogOn?returnUrl=/Home/About . El ataque, a continuación, sigue estos pasos:

1. Usuario hace clic en un vínculo a


http://nerddinner.com/Account/LogOn?returnUrl=http://nerddiner.com/Account/LogOn (tenga en cuenta, segunda
dirección URL es nerddiner, no nerddinner).
2. El usuario inicia sesión correctamente.
3. Se redirige al usuario (en el sitio) a http://nerddiner.com/Account/LogOn (sitio malintencionado que parece sitio
real).
4. El usuario inicia sesión de nuevo (dando malintencionado sus credenciales de sitio) y se le redirige al sitio real.
El usuario es probable que cree su primer intento de iniciar sesión no se pudo y la otra se realizó correctamente.
Probablemente permanecerá sin tener en cuenta sus credenciales se han visto comprometidas.
Además de las páginas de inicio de sesión, algunos sitios proporcionan páginas de redireccionamiento o puntos de
conexión. Imagine que la aplicación tiene una página con una redirección abierta, /Home/Redirect . Un atacante
podría crear, por ejemplo, un vínculo en un correo electrónico que se va a
[yoursite]/Home/Redirect?url=http://phishingsite.com/Home/Login . Un usuario típico en la dirección URL y saber
que comienza con el nombre del sitio. Confiar en, hará clic en el vínculo. La redirección abierta enviaría a
continuación, el usuario para el sitio de suplantación de identidad, cuya apariencia es idéntico a la suya, y es
probable que lo haría el usuario inicie sesión en lo creen que es su sitio.

Protegerse contra los ataques de redirección abierta


Al desarrollar aplicaciones web, trate todos los datos proporcionados por el usuario que no es de confianza. Si la
aplicación dispone de funcionalidad que redirige al usuario según el contenido de la dirección URL, asegúrese de
que estas redirecciones solo se realizan localmente dentro de la aplicación (o a una dirección URL conocida, no
cualquier dirección URL que puede especificarse en la cadena de consulta).
LocalRedirect
Use la LocalRedirect método auxiliar de la base de Controller clase:

public IActionResult SomeAction(string redirectUrl)


{
return LocalRedirect(redirectUrl);
}

LocalRedirect se iniciará una excepción si se especifica una dirección URL no locales. En caso contrario, se
comporta igual que el Redirect método.
IsLocalUrl
Use la IsLocalUrl método para probar las direcciones URL antes de redirigir:
En el ejemplo siguiente se muestra cómo comprobar si una dirección URL es local antes de redirigir.

private IActionResult RedirectToLocal(string returnUrl)


{
if (Url.IsLocalUrl(returnUrl))
{
return Redirect(returnUrl);
}
else
{
return RedirectToAction(nameof(HomeController.Index), "Home");
}
}

El IsLocalUrl método protege a los usuarios sin darse cuenta sea redirigido a un sitio malintencionado. Puede
registrar los detalles de la dirección URL que se proporcionó cuando se proporciona una dirección URL no es local
en una situación donde se espera una dirección URL local. Registro de redirección de direcciones URL pueden
ayudar a diagnosticar los ataques de redirección.
Evitar que las secuencias de comandos (XSS) en
ASP.NET Core
22/06/2018 • 13 minutes to read • Edit Online

Por Rick Anderson


Scripting entre sitios (XSS ) son una vulnerabilidad de seguridad que permite a un atacante colocar scripts del
lado cliente (normalmente JavaScript) en las páginas web. Cuando otros usuarios cargar páginas afectadas se
ejecutarán las secuencias de comandos de los atacantes, lo que podría robar cookies y tokens de sesión, cambiar
el contenido de la página web mediante la manipulación de DOM o redirigir el explorador a otra página.
Vulnerabilidades XSS suelen producen cuando una aplicación toma la entrada del usuario y genera en una
página sin validación, codificación o secuencias de escape.

Protección de la aplicación en XSS


AT un XSS de nivel básico funciona engañar a su aplicación en Insertar un <script> etiqueta en la página
representada, o insertando una On* eventos en un elemento. Los desarrolladores deben usar los siguientes
pasos de prevención para evitar la introducción XSS en su aplicación.
1. Nunca Coloque datos que no se confía en la entrada HTML, a menos que siga el resto de los pasos
siguientes. Datos no es de confianza están cualquier dato que puede controlarse mediante un atacante,
entradas de formulario HTML, las cadenas de consulta, los encabezados HTTP, ni siquiera los datos
proceden de una base de datos como un atacante puede infringir la base de datos aunque no pueden
infringir la aplicación.
2. Antes de poner los datos no es de confianza dentro de un elemento HTML Asegúrese de que está
codificado en HTML. Codificación de HTML tiene caracteres como < y se modifican de forma segura como
&lt;
3. Antes de poner los datos que no se confía en un atributo HTML Asegúrese sea atributo HTML codificado.
Codificación del atributo HTML es un supraconjunto de codificación HTML y codifica los caracteres
adicionales como "y".
4. Antes de poner los datos que no se confía en JavaScript colocar los datos en un elemento HTML cuyo
contenido recuperar en tiempo de ejecución. Si esto no es posible garantizar que los datos se codifica
JavaScript. Codificación de JavaScript tiene caracteres peligrosos para JavaScript y los reemplaza con su
hexadecimal, por ejemplo < se codifica como \u003C .
5. Antes de poner los datos que no se confía en una cadena de consulta de dirección URL Asegúrese de que
es la dirección URL codificada.

Codificación HTML con Razor


El motor de Razor usado en MVC automáticamente codifica todos los resultados como origen de las variables, a
menos que trabaje mucho para evitar que esto. Usa el atributo HTML reglas de codificación, siempre que se use
la @ directiva. Como HTML la codificación del atributo es un supraconjunto de codificación HTML, que esto
significa que no tiene que preocuparse de si debe utilizar la codificación HTML o codificación del atributo HTML.
Debe asegurarse de que utiliza solo en un contexto HTML, no al intentar insertar la entrada que no se confía
directamente en JavaScript. Aplicaciones auxiliares de etiquetas codificará también usen en parámetros de la
etiqueta de entrada.
Realizar la siguiente vista Razor;

@{
var untrustedInput = "<\"123\">";
}

@untrustedInput

Esta vista muestra el contenido de la untrustedInput variable. Esta variable incluye algunos caracteres que se usan
en los ataques XSS, es decir, <, "y >. Examinar el código fuente, muestra el resultado representado codificado
como:

&lt;&quot;123&quot;&gt;

WARNING
Núcleo ASP.NET MVC proporciona una HtmlString clase que no está codificado automáticamente tras la salida. Esto
nunca debe utilizarse en combinación con la entrada no es de confianza como esto expondrá una vulnerabilidad XSS.

Codificación de JavaScript con Razor


Puede haber ocasiones en que desea insertar un valor en JavaScript para procesar en la vista. Hay dos formas de
hacerlo. La forma más segura para insertar valores es colocar el valor en un atributo de una etiqueta de datos y
recuperarlos en el JavaScript. Por ejemplo:

@{
var untrustedInput = "<\"123\">";
}

<div
id="injectedData"
data-untrustedinput="@untrustedInput" />

<script>
var injectedData = document.getElementById("injectedData");

// All clients
var clientSideUntrustedInputOldStyle =
injectedData.getAttribute("data-untrustedinput");

// HTML 5 clients only


var clientSideUntrustedInputHtml5 =
injectedData.dataset.untrustedinput;

document.write(clientSideUntrustedInputOldStyle);
document.write("<br />")
document.write(clientSideUntrustedInputHtml5);
</script>

Esto generará el siguiente código HTML


<div
id="injectedData"
data-untrustedinput="&lt;&quot;123&quot;&gt;" />

<script>
var injectedData = document.getElementById("injectedData");

var clientSideUntrustedInputOldStyle =
injectedData.getAttribute("data-untrustedinput");

var clientSideUntrustedInputHtml5 =
injectedData.dataset.untrustedinput;

document.write(clientSideUntrustedInputOldStyle);
document.write("<br />")
document.write(clientSideUntrustedInputHtml5);
</script>

Que, cuando se ejecuta, representarán lo siguiente;

<"123">
<"123">

También puede llamar directamente, el codificador de JavaScript

@using System.Text.Encodings.Web;
@inject JavaScriptEncoder encoder;

@{
var untrustedInput = "<\"123\">";
}

<script>
document.write("@encoder.Encode(untrustedInput)");
</script>

Esto se representará en el Explorador de manera;

<script>
document.write("\u003C\u0022123\u0022\u003E");
</script>

WARNING
No concatene que no se confía en JavaScript para crear elementos DOM. Debe usar createElement() y asignar valores de
propiedad correctamente como node.TextContent= , o use element.SetAttribute() / element[attribute]= en caso
contrario, se expone a DOM-based XSS.

Obtener acceso a los codificadores en código


Los codificadores HTML, JavaScript y URL están disponibles en el código de dos maneras, también puede
insertar ellos a través de inyección de dependencia o puede usar los codificadores predeterminados incluidos en
el System.Text.Encodings.Web espacio de nombres. Si usas los codificadores de manera predeterminada, a
continuación, aplicado a los intervalos de caracteres se traten como seguros no surtirán efecto: los codificadores
predeterminado usen las reglas de codificación más seguras posible.
Para usar los codificadores configurables a través de DI deben tomar los constructores de una HtmlEncoder,
JavaScriptEncoder y UrlEncoder parámetro según corresponda. Por ejemplo,

public class HomeController : Controller


{
HtmlEncoder _htmlEncoder;
JavaScriptEncoder _javaScriptEncoder;
UrlEncoder _urlEncoder;

public HomeController(HtmlEncoder htmlEncoder,


JavaScriptEncoder javascriptEncoder,
UrlEncoder urlEncoder)
{
_htmlEncoder = htmlEncoder;
_javaScriptEncoder = javascriptEncoder;
_urlEncoder = urlEncoder;
}
}

Parámetros de codificación de dirección URL


Si desea compilar una cadena de consulta de dirección URL con una entrada no es de confianza como un valor,
use la UrlEncoder para codificar el valor. Por ejemplo,

var example = "\"Quoted Value with spaces and &\"";


var encodedValue = _urlEncoder.Encode(example);

Después de la codificación del encodedValue variable contendrá


%22Quoted%20Value%20with%20spaces%20and%20%26%22 . Espacios, comillas, signos de puntuación y otros caracteres no
seguros se porcentaje codificados en su valor hexadecimal, por ejemplo un carácter de espacio se convertirá en %
20.

WARNING
No use la entrada no es de confianza como parte de una ruta de acceso de dirección URL. Siempre pase datos
proporcionados no es de confianza como un valor de cadena de consulta.

Personalizar los codificadores


De forma predeterminada codificadores utiliza una lista segura limitada en el intervalo de Unicode Latín básico y
codifican todos los caracteres fuera de ese intervalo como sus equivalentes de código de carácter. Este
comportamiento también afecta a la representación TagHelper Razor y HtmlHelper como utilizarán los
codificadores a sus cadenas de salida.
El razonamiento sirve como protección ante errores desconocidos o futuros explorador (errores de explorador
anterior se desencadena una análisis basado en el procesamiento de caracteres no válidos). Si el sitio web hace
un uso intensivo de los caracteres no latinos, como el chino, cirílico u otros Esto probablemente no es el
comportamiento que desee.
Puede personalizar las listas seguras de codificador para incluir Unicode intervalos apropiados para la aplicación
durante el inicio, en ConfigureServices() .
Por ejemplo, mediante la configuración predeterminada que se puede usar un HtmlHelper Razor así;
<p>This link text is in Chinese: @Html.ActionLink("汉语/漢語", "Index")</p>

Al ver el origen de la página web verá que se ha representado como sigue, con el texto en chino codificado;

<p>This link text is in Chinese: <a href="/">&#x6C49;&#x8BED;/&#x6F22;&#x8A9E;</a></p>

Para ampliar los caracteres que se tratan como seguro para el codificador se insertaría la línea siguiente en el
ConfigureServices() método startup.cs ;

services.AddSingleton<HtmlEncoder>(
HtmlEncoder.Create(allowedRanges: new[] { UnicodeRanges.BasicLatin,
UnicodeRanges.CjkUnifiedIdeographs }));

En este ejemplo se amplía la lista segura para que incluya la CjkUnifiedIdeographs de intervalo de Unicode.
Ahora se convertiría en la salida representada

<p>This link text is in Chinese: <a href="/">汉语/漢語</a></p>

Intervalos de lista segura se especifican como gráficos de código Unicode, no los idiomas. El estándar Unicode
tiene una lista de gráficos de código puede usar para buscar el gráfico que contiene los caracteres. Cada
codificador, Html, JavaScript y dirección Url, debe configurarse por separado.

NOTE
Personalización de la lista segura sólo afecta a los codificadores de origen a través de DI. Si tiene acceso directamente a un
codificador a través de System.Text.Encodings.Web.*Encoder.Default , a continuación, el valor predeterminado, Latín
básico se usará sólo lista segura.

¿Dónde debe colocar tienen codificación?


La ficha general acepta práctica es que la codificación realiza en el punto de salida y valores codificados nunca
deben almacenarse en una base de datos. Codificación en el punto de salida permite cambiar el uso de datos, por
ejemplo, de HTML a un valor de cadena de consulta. También le permite buscar fácilmente los datos sin tener que
codificar valores antes de buscar y le permite aprovechar las ventajas de las modificaciones o intentados
codificadores de correcciones de errores.

Validación como una técnica de prevención de XSS


Validación puede ser una herramienta útil para limitar los ataques XSS. Por ejemplo, un tipo numérico string que
contiene los caracteres 0-9 no desencadenará un ataque XSS. Validación se complica si desea aceptar HTML en
proporcionados por el usuario - analiza la entrada de HTML es difícil, si no imposible. Otros formatos de texto y
marcado sería una opción más segura para la entrada enriquecido. Nunca debe confiar en la validación por sí
sola. Codifique siempre la entrada no es de confianza antes de la salida, con independencia de qué validación ha
llevado a cabo.
Permitir solicitudes entre orígenes (CORS) en
ASP.NET Core
22/06/2018 • 16 minutes to read • Edit Online

Por Mike Wasson, Shayne Boyer, y Tom Dykstra


La seguridad del explorador impide que una página web realice solicitudes AJAX a otro dominio. Esta restricción
se denomina "directiva de mismo origen" e impide que un sitio malintencionado lea los datos confidenciales
desde otro sitio. En cambio, a veces, es posible que quiera permitir que otros sitios realicen solicitudes entre
orígenes en su API web.
El uso compartido de recursos entre orígenes (CORS ) es un estándar del W3C que permite que un servidor
modere la directiva de mismo origen. Mediante CORS, un servidor puede permitir explícitamente algunas
solicitudes entre orígenes y rechazar otras. CORS es más seguro y flexible que técnicas anteriores como JSONP.
En este tema, se muestra cómo habilitar CORS en una aplicación de ASP.NET Core.

¿Qué es el "mismo origen"?


Dos direcciones URL tienen el mismo origen si tienen puertos, hosts y esquemas idénticos. (RFC 6454)
Estas dos direcciones URL tienen el mismo origen:
http://example.com/foo.html

http://example.com/bar.html

Estas direcciones URL tienen orígenes diferentes de los dos anteriores:


http://example.net : dominio diferente
http://www.example.com/foo.html : subdominio diferente
https://example.com/foo.html : esquema diferente
http://example.com:9000/foo.html : puerto diferente

NOTE
Internet Explorer no tiene en cuenta el puerto al comparar orígenes.

Configuración de CORS
Para configurar CORS en su aplicación, agregue el paquete Microsoft.AspNetCore.Cors al proyecto.
Agregue los servicios CORS en Startup.cs:

public void ConfigureServices(IServiceCollection services)


{
services.AddCors();
}
Habilitación de CORS con middleware
Para habilitar CORS para toda la aplicación, agregue el middleware de CORS a la canalización de solicitud
mediante el método de extensión UseCors . Tenga en cuenta que el middleware de CORS debe preceder a
cualquier punto de conexión definido en la aplicación que quiera que admita las solicitudes entre orígenes (por
ejemplo, antes de cualquier llamada a UseMvc ).
Puede especificar una directiva entre orígenes al agregar el middleware de CORS mediante la clase
CorsPolicyBuilder . Hay dos formas de hacerlo. La primera consiste en llamar a UseCors con una expresión
lambda:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)


{
loggerFactory.AddConsole();

if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}

// Shows UseCors with CorsPolicyBuilder.


app.UseCors(builder =>
builder.WithOrigins("http://example.com"));

app.Run(async (context) =>


{
await context.Response.WriteAsync("Hello World!");
});

Nota: Debe especificarse la dirección URL sin una barra diagonal final ( / ). Si la dirección URL finaliza con / ,
la comparación devolverá false y no se devolverá ningún encabezado.
La expresión lambda toma un objeto CorsPolicyBuilder . Encontrará una lista de las opciones de configuración
más adelante en este tema. En este ejemplo, la directiva permite las solicitudes entre orígenes de
http://example.com y no de otros orígenes.

Tenga en cuenta que CorsPolicyBuilder tiene una API fluida, por lo que se pueden encadenar llamadas de
método:

app.UseCors(builder =>
builder.WithOrigins("http://example.com")
.AllowAnyHeader()
);

El segundo enfoque es definir una o varias directivas CORS con nombre y, después, seleccionar la directiva por
su nombre en tiempo de ejecución.
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(options =>
{
options.AddPolicy("AllowSpecificOrigin",
builder => builder.WithOrigins("http://example.com"));
});
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)


{
loggerFactory.AddConsole();

if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}

// Shows UseCors with named policy.


app.UseCors("AllowSpecificOrigin");
app.Run(async (context) =>
{
await context.Response.WriteAsync("Hello World!");
});
}

Este ejemplo agrega una directiva CORS denominada "AllowSpecificOrigin". Para seleccionar la directiva, pase el
nombre a UseCors .

Habilitación de CORS en MVC


Como alternativa, puede usar MVC para aplicar CORS específico por cada acción, por cada controlador o
globalmente para todos los controladores. Al utilizar MVC para habilitar CORS se utilizan los mismos servicios
CORS, pero no el middleware CORS.
Por cada acción
Para especificar una directiva CORS para una acción específica, agregue el atributo [EnableCors] a la acción.
Especifique el nombre de la directiva.

[HttpGet]
[EnableCors("AllowSpecificOrigin")]
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}

Por cada controlador


Para especificar la directiva CORS para un controlador específico, agregue el atributo [EnableCors] a la clase
controller. Especifique el nombre de la directiva.

[Route("api/[controller]")]
[EnableCors("AllowSpecificOrigin")]
public class ValuesController : Controller

Global
Puede habilitar CORS globalmente para todos los controladores si agrega el filtro
CorsAuthorizationFilterFactory a la colección de filtros globales:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.Configure<MvcOptions>(options =>
{
options.Filters.Add(new CorsAuthorizationFilterFactory("AllowSpecificOrigin"));
});
}

El orden de prioridad es: acción, controlador, global. Las directivas de nivel de acción tienen prioridad sobre las
directivas de nivel de controlador y estas últimas tienen prioridad sobre las directivas globales.
Deshabilitar CORS
Para deshabilitar CORS para una acción o un controlador, use el atributo [DisableCors] .

[HttpGet("{id}")]
[DisableCors]
public string Get(int id)
{
return "value";
}

Opciones de directiva CORS


Esta sección describe las distintas opciones que se pueden establecer en una directiva CORS.
Establecer los orígenes permitidos
Establecer los métodos HTTP permitidos
Establecer los encabezados de solicitudes permitidos
Establecer los encabezados de respuesta expuestos
Credenciales en solicitudes cross-origin
Establecer el tiempo de expiración de las comprobaciones preparatorias
Para algunas opciones pueden serle de ayuda leer CORS cómo funciona primero.
Establecer los orígenes permitidos
Para permitir uno o más orígenes específicos:

options.AddPolicy("AllowSpecificOrigins",
builder =>
{
builder.WithOrigins("http://example.com", "http://www.contoso.com");
});

Para permitir todos los orígenes:

using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace CorsExample4
{
{
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?
LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(options =>
{
// BEGIN01
options.AddPolicy("AllowSpecificOrigins",
builder =>
{
builder.WithOrigins("http://example.com", "http://www.contoso.com");
});
// END01

// BEGIN02
options.AddPolicy("AllowAllOrigins",
builder =>
{
builder.AllowAnyOrigin();
});
// END02

// BEGIN03
options.AddPolicy("AllowSpecificMethods",
builder =>
{
builder.WithOrigins("http://example.com")
.WithMethods("GET", "POST", "HEAD");
});
// END03

// BEGIN04
options.AddPolicy("AllowAllMethods",
builder =>
{
builder.WithOrigins("http://example.com")
.AllowAnyMethod();
});
// END04

// BEGIN05
options.AddPolicy("AllowHeaders",
builder =>
{
builder.WithOrigins("http://example.com")
.WithHeaders("accept", "content-type", "origin", "x-custom-header");
});
// END05

// BEGIN06
options.AddPolicy("AllowAllHeaders",
builder =>
{
builder.WithOrigins("http://example.com")
.AllowAnyHeader();
});
// END06

// BEGIN07
options.AddPolicy("ExposeResponseHeaders",
builder =>
{
builder.WithOrigins("http://example.com")
.WithExposedHeaders("x-custom-header");
});
// END07

// BEGIN08
options.AddPolicy("AllowCredentials",
builder =>
{
builder.WithOrigins("http://example.com")
.AllowCredentials();
});
// END08

// BEGIN09
options.AddPolicy("SetPreflightExpiration",
builder =>
{
builder.WithOrigins("http://example.com")
.SetPreflightMaxAge(TimeSpan.FromSeconds(2520));
});
// END09
});
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole();

if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}

app.UseCors("AllowSpecificOrigins");
app.Run(async (context) =>
{
await context.Response.WriteAsync("Hello World!");
});
}
}
}

Debe considerar detenidamente antes de permitir que las solicitudes de cualquier origen. Significa que
literalmente cualquier sitio Web puede realizar las llamadas de AJAX a su API.
Establecer los métodos HTTP permitidos
Para permitir todos los métodos HTTP:

options.AddPolicy("AllowAllMethods",
builder =>
{
builder.WithOrigins("http://example.com")
.AllowAnyMethod();
});

Esto afecta a las solicitudes de comprobaciones preparatorias y al encabezado Access-Control-Allow -Methods.


Establecer los encabezados de solicitudes permitidos
Una solicitud de comprobaciones preparatorias de CORS puede incluir un encabezado Access-Control-Request-
Headers, en el que aparezcan los encabezados HTTP que ha establecido la aplicación (los denominados
"encabezados de solicitud de autor").
Para agregar encabezados específicos a la lista de permitidos:
options.AddPolicy("AllowHeaders",
builder =>
{
builder.WithOrigins("http://example.com")
.WithHeaders("accept", "content-type", "origin", "x-custom-header");
});

Para permitir todos los encabezados de solicitud de autor:

options.AddPolicy("AllowAllHeaders",
builder =>
{
builder.WithOrigins("http://example.com")
.AllowAnyHeader();
});

Los exploradores no son completamente coherentes en la forma en que establecen Access-Control-Request-


Headers. Si establece encabezados en un valor que no sea "*", debe incluir al menos "accept", "content-type" y
"origin", además de los encabezados personalizados que quiera admitir.
Establecer los encabezados de respuesta expuestos
De forma predeterminada, el explorador no expone todos los encabezados de respuesta a la aplicación. (Consulte
http://www.w3.org/TR/cors/#simple-response-header .) Los encabezados de respuesta que están disponibles de
forma predeterminada son:
Control de caché
Content-Language
Content-Type
Expires
Last-Modified
Pragma
La especificación CORS llama a estos encabezados de respuesta simple. Para que otros encabezados estén
disponibles para la aplicación:

options.AddPolicy("ExposeResponseHeaders",
builder =>
{
builder.WithOrigins("http://example.com")
.WithExposedHeaders("x-custom-header");
});

Credenciales en solicitudes cross-origin


Las credenciales requieren un tratamiento especial en una solicitud de CORS. De forma predeterminada, el
explorador no envía las credenciales con una solicitud entre orígenes. Las credenciales son las cookies, así como
esquemas de autenticación HTTP. Para enviar las credenciales con una solicitud entre orígenes, el cliente debe
establecer XMLHttpRequest.withCredentials en true.
Al usar directamente el objeto XMLHttpRequest:
var xhr = new XMLHttpRequest();
xhr.open('get', 'http://www.example.com/api/test');
xhr.withCredentials = true;

En jQuery:

$.ajax({
type: 'get',
url: 'http://www.example.com/home',
xhrFields: {
withCredentials: true
}

Además, el servidor debe permitir las credenciales. Para permitir las credenciales entre orígenes:

options.AddPolicy("AllowCredentials",
builder =>
{
builder.WithOrigins("http://example.com")
.AllowCredentials();
});

Ahora, la respuesta HTTP incluirá un encabezado Access-Control-Allow -Credentials, que indica al explorador
que el servidor permite credenciales para una solicitud entre orígenes.
Si el explorador envía las credenciales, pero la respuesta no incluye un encabezado Access-Control-Allow -
Credentials válido, el explorador no expone la respuesta a la aplicación y se produce un error en la solicitud de
AJAX.
Tenga cuidado al permitir credenciales entre orígenes. Un sitio web en otro dominio puede enviar las
credenciales de un usuario que ha iniciado sesión a la aplicación en nombre del usuario sin su conocimiento. La
especificación de CORS también indica que configurar los orígenes en "*" (todos los orígenes) no es válido si
está presente el encabezado Access-Control-Allow-Credentials .
Establecer el tiempo de expiración de las comprobaciones preparatorias
El encabezado Access-Control-Max-Age especifica durante cuánto tiempo puede almacenarse en caché la
respuesta a la solicitud de comprobaciones preparatorias. Para establecer este encabezado:

options.AddPolicy("SetPreflightExpiration",
builder =>
{
builder.WithOrigins("http://example.com")
.SetPreflightMaxAge(TimeSpan.FromSeconds(2520));
});

Funcionamiento de CORS
Esta sección describe lo que ocurre en una solicitud de CORS en el nivel de los mensajes HTTP. Es importante
comprender el funcionamiento de CORS para que se pueda configurar correctamente la directiva CORS y se
puedan solucionar los problemas cuando se produzcan comportamientos inesperados.
La especificación de CORS presenta varios encabezados HTTP nuevos que permiten las solicitudes entre
orígenes. Si un explorador es compatible con CORS, establece estos encabezados automáticamente para las
solicitudes entre orígenes. No se necesita código JavaScript personalizado para habilitar CORS.
Este es un ejemplo de una solicitud entre orígenes. El encabezado Origin proporciona el dominio del sitio que
está realizando la solicitud:

GET http://myservice.azurewebsites.net/api/test HTTP/1.1


Referer: http://myclient.azurewebsites.net/
Accept: */*
Accept-Language: en-US
Origin: http://myclient.azurewebsites.net
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; WOW64; Trident/6.0)
Host: myservice.azurewebsites.net

Si el servidor permite la solicitud, establece el encabezado Access-Control-Allow -Origin en la respuesta. El valor


de este encabezado coincide con el encabezado de origen de la solicitud, o es el valor de carácter comodín "*", lo
que significa que se permite cualquier origen:

HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: text/plain; charset=utf-8
Access-Control-Allow-Origin: http://myclient.azurewebsites.net
Date: Wed, 20 May 2015 06:27:30 GMT
Content-Length: 12

Test message

Si la respuesta no incluye el encabezado Access-Control-Allow -Origin, se produce un error en la solicitud de


AJAX. En concreto, el explorador no permite la solicitud. Incluso si el servidor devuelve una respuesta correcta, el
explorador no expone la respuesta a la aplicación cliente.
Solicitudes preparatorias
Para algunas solicitudes CORS, el explorador envía una solicitud adicional, denominada "solicitud preparatoria,"
antes de enviar la solicitud real para el recurso. El explorador puede omitir la solicitud preparatoria si se cumplen
las condiciones siguientes:
El método de solicitud es GET, HEAD o POST, y
La aplicación no establece los encabezados de solicitud que no sean Accept, Accept-Language, Content-
Language, Content-Type o Last-Event-ID, y
El encabezado Content-Type (si se establece) es uno de los siguientes:
application/x-www -form-urlencoded
varias partes/de datos de formulario
text/plain
La regla sobre los encabezados de solicitud se aplica a los encabezados que la aplicación establece mediante una
llamada a setRequestHeader en el objeto XMLHttpRequest. (La especificación de CORS llama a estos
"encabezados de solicitud de autor"). La regla no se aplica a los encabezados que puede establecer el explorador,
como User-Agent, Host o Content-Length.
Este es un ejemplo de una solicitud preparatoria:
OPTIONS http://myservice.azurewebsites.net/api/test HTTP/1.1
Accept: */*
Origin: http://myclient.azurewebsites.net
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: accept, x-my-custom-header
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; WOW64; Trident/6.0)
Host: myservice.azurewebsites.net
Content-Length: 0

La solicitud preparatoria utiliza el método HTTP OPTIONS. Incluye dos encabezados especiales:
Access-Control-Request-Method: el método HTTP que se usará para la solicitud real.
Access-Control-Request-Headers: una lista de encabezados de solicitud que la aplicación establece en la
solicitud real. (De nuevo, esto no incluye los encabezados que establece el explorador).
Esta es una respuesta de ejemplo, suponiendo que el servidor permite que la solicitud:

HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Length: 0
Access-Control-Allow-Origin: http://myclient.azurewebsites.net
Access-Control-Allow-Headers: x-my-custom-header
Access-Control-Allow-Methods: PUT
Date: Wed, 20 May 2015 06:33:22 GMT

La respuesta incluye un encabezado Access-Control-Allow -Methods que enumera los métodos permitidos y,
opcionalmente, un encabezado Access-Control-Allow -Headers, que muestra los encabezados permitidos. Si la
solicitud de comprobaciones preparatorias se realiza correctamente, el explorador envía la solicitud real, como se
ha descrito anteriormente.
Compartir cookies entre aplicaciones con ASP.NET y
ASP.NET Core
22/06/2018 • 10 minutes to read • Edit Online

Por Rick Anderson y Luke Latham


Sitios Web a menudo constan de las aplicaciones web individuales que trabajan juntos. Para proporcionar una
experiencia de inicio de sesión único (SSO ), las aplicaciones web dentro de un sitio deben compartir las cookies de
autenticación. Para admitir este escenario, la pila de protección de datos permite compartir la autenticación con
cookies Katana y vales de autenticación de ASP.NET Core cookie.
Vea o descargue el código de ejemplo (cómo descargarlo)
El ejemplo ilustra la cookie compartir entre tres aplicaciones que usan autenticación con cookies:
Aplicación de las páginas de Razor de núcleo 2.0 de ASP.NET sin usar ASP.NET Core Identity
Aplicación MVC de ASP.NET Core 2.0 con la identidad de ASP.NET Core
Aplicación MVC de ASP.NET Framework 4.6.1 con la identidad de ASP.NET
En los ejemplos siguientes:
El nombre de la cookie de autenticación se establece en un valor común de .AspNet.SharedCookie .
El AuthenticationType está establecido en Identity.Application explícitamente o de forma predeterminada.
Un nombre de aplicación común se utiliza para habilitar el sistema de protección de datos compartir las claves
de protección de datos ( SharedCookieApp ).
Identity.Application se utiliza como el esquema de autenticación. Se utiliza el esquema, se debe usar de forma
coherente dentro y entre las aplicaciones de cookie compartido como el esquema predeterminado o si se
establece explícitamente. El esquema se utiliza al cifrar y descifrar las cookies, por lo que se debe usar un
esquema coherente entre aplicaciones.
Común clave de protección de datos se utiliza la ubicación de almacenamiento. La aplicación de ejemplo utiliza
una carpeta denominada KeyRing en la raíz de la solución para almacenar las claves de protección de datos.
En las aplicaciones ASP.NET Core, PersistKeysToFileSystem se usa para establecer la ubicación de
almacenamiento de claves. SetApplicationName se usa para configurar un nombre de aplicación compartido
común.
En la aplicación de .NET Framework, el middleware de autenticación de cookie usa una implementación de
DataProtectionProvider. DataProtectionProvider proporciona servicios de protección de datos para el cifrado y
descifrado de datos de carga de cookie de autenticación. El DataProtectionProvider instancia está aislada del
sistema de protección de datos utilizado por otras partes de la aplicación.
DataProtectionProvider.Create (System.IO.DirectoryInfo, acción<IDataProtectionBuilder >) acepta un
DirectoryInfo para especificar la ubicación de almacenamiento de claves de protección de datos. La
aplicación de ejemplo proporciona la ruta de acceso de la KeyRing carpeta DirectoryInfo .
DataProtectionBuilderExtensions.SetApplicationName establece el nombre de aplicación común.
DataProtectionProvider requiere el Microsoft.AspNetCore.DataProtection.Extensions paquete NuGet.
Para obtener este paquete de aplicaciones más adelante y ASP.NET Core 2.1, hacen referencia a la
Microsoft.AspNetCore.App metapackage. Cuando el destino es .NET Framework, agregue una
referencia de paquete a Microsoft.AspNetCore.DataProtection.Extensions .

Compartir las cookies de autenticación entre aplicaciones de ASP.NET


Core
Al utilizar la identidad de núcleo de ASP.NET:
ASP.NET Core 2.x
ASP.NET Core 1.x
En el ConfigureServices método, use la ConfigureApplicationCookie método de extensión para configurar el
servicio de protección de datos para las cookies.

services.AddDataProtection()
.PersistKeysToFileSystem(GetKeyRingDirInfo())
.SetApplicationName("SharedCookieApp");

services.ConfigureApplicationCookie(options => {
options.Cookie.Name = ".AspNet.SharedCookie";
});

Las claves de protección de datos y el nombre de la aplicación deben compartirse entre aplicaciones. En las
aplicaciones de ejemplo, GetKeyRingDirInfo devuelve la ubicación de almacenamiento de claves comunes para la
PersistKeysToFileSystem método. Use SetApplicationName para configurar un nombre de aplicación compartido
común ( SharedCookieApp en el ejemplo). Para obtener más información, consulte configurar la protección de datos.
Consulte la CookieAuthWithIdentity.Core del proyecto en el código de ejemplo (cómo descargar).
Al utilizar cookies directamente:
ASP.NET Core 2.x
ASP.NET Core 1.x

services.AddDataProtection()
.PersistKeysToFileSystem(GetKeyRingDirInfo())
.SetApplicationName("SharedCookieApp");

services.AddAuthentication("Identity.Application")
.AddCookie("Identity.Application", options =>
{
options.Cookie.Name = ".AspNet.SharedCookie";
});

Las claves de protección de datos y el nombre de la aplicación deben compartirse entre aplicaciones. En las
aplicaciones de ejemplo, GetKeyRingDirInfo devuelve la ubicación de almacenamiento de claves comunes para la
PersistKeysToFileSystem método. Use SetApplicationName para configurar un nombre de aplicación compartido
común ( SharedCookieApp en el ejemplo). Para obtener más información, consulte configurar la protección de datos.
Consulte la CookieAuth.Core del proyecto en el código de ejemplo (cómo descargar).

Cifrado de claves de protección de datos en reposo


Para las implementaciones de producción, configurar el DataProtectionProvider para cifrar las claves en reposo
con DPAPI o un X509Certificate. Vea clave de cifrado de datos almacenados para obtener más información.
ASP.NET Core 2.x
ASP.NET Core 1.x
services.AddDataProtection()
.ProtectKeysWithCertificate("thumbprint");

Uso compartido de las cookies de autenticación entre ASP.NET 4.x y las


aplicaciones de ASP.NET Core
Las aplicaciones ASP.NET 4.x que usar el middleware de autenticación de cookie de Katana pueden configurarse
para generar las cookies de autenticación que son compatibles con el middleware de autenticación de la cookie de
ASP.NET Core. Esto permite actualizar aplicaciones individuales de un sitio grande por etapas al proporcionar una
experiencia SSO sin problemas en todo el sitio.
Cuando una aplicación usa middleware de autenticación de cookie de Katana, llama al método
UseCookieAuthentication en el proyecto Startup.Auth.cs archivo. Proyectos de aplicación web de ASP.NET 4.x crean
con Visual Studio 2013 y utilizan el middleware de autenticación de la cookie de Katana de forma predeterminada.
Aunque UseCookieAuthentication está obsoleto y no compatibles para las aplicaciones de ASP.NET Core, una
llamada a UseCookieAuthentication en una aplicación ASP.NET 4.x que usa Katana middleware de autenticación de
la cookie es válido.
Una aplicación ASP.NET 4.x debe tener como destino .NET Framework 4.5.1 o posterior. En caso contrario, los
paquetes de NuGet necesarios no puede instalar.
Para compartir las cookies de autenticación entre una aplicación ASP.NET 4.x y una aplicación de ASP.NET Core,
configure la aplicación de ASP.NET Core tal y como se ha indicado anteriormente, después de configurar la
aplicación ASP.NET 4.x siguiendo estos pasos:
1. Instalar el paquete Microsoft.Owin.Security.Interop en cada aplicación ASP.NET 4.x.
2. En Startup.Auth.cs, busque la llamada a UseCookieAuthentication y modifíquelo como se indica a
continuación. Cambie el nombre de cookie para que coincida con el nombre utilizado por el middleware de
autenticación de la cookie de ASP.NET Core. Proporcionar una instancia de un DataProtectionProvider
inicializado para la ubicación de almacenamiento de claves de protección de datos comunes. Asegúrese de
que el nombre de la aplicación se establece en el nombre de aplicación común utilizado por todas las
aplicaciones que comparten las cookies, SharedCookieApp en la aplicación de ejemplo.
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Identity.Application",
CookieName = ".AspNet.SharedCookie",
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider
{
OnValidateIdentity =
SecurityStampValidator
.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
validateInterval: TimeSpan.FromMinutes(30),
regenerateIdentity: (manager, user) =>
user.GenerateUserIdentityAsync(manager))
},
TicketDataFormat = new AspNetTicketDataFormat(
new DataProtectorShim(
DataProtectionProvider.Create(GetKeyRingDirInfo(),
(builder) => { builder.SetApplicationName("SharedCookieApp"); })
.CreateProtector(
"Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware",
"Identity.Application",
"v2"))),
CookieManager = new ChunkingCookieManager()
});

// If not setting http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier and


// http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider,
// then set UniqueClaimTypeIdentifier to a claim that distinguishes unique users.
System.Web.Helpers.AntiForgeryConfig.UniqueClaimTypeIdentifier =
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name";

Consulte la CookieAuthWithIdentity.NETFramework del proyecto en el código de ejemplo (cómo descargar).


Al generar una identidad de usuario, el tipo de autenticación debe coincidir con el tipo definido en
AuthenticationType establecido con UseCookieAuthentication .

Models/IdentityModels.cs:

public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager)


{
// Note the authenticationType must match the one defined in
CookieAuthenticationOptions.AuthenticationType
var userIdentity = await manager.CreateIdentityAsync(this, "Identity.Application");
// Add custom user claims here
return userIdentity;
}

Usar una base de datos de usuario común


Confirme que el sistema de identidad para cada aplicación apunta a la misma base de datos de usuario. En caso
contrario, el sistema de identidades produce errores en tiempo de ejecución cuando intenta hacer coincidir la
información de la cookie de autenticación con la información de su base de datos.
Rendimiento en ASP.NET Core
21/06/2018 • 2 minutes to read • Edit Online

Almacenamiento en caché de respuestas


Almacenamiento en caché en memoria
Trabajar con una memoria caché distribuida
Almacenamiento en caché de respuestas
Middleware de compresión de respuestas
Almacenamiento en caché de respuestas en ASP.NET
Core
19/06/2018 • 2 minutes to read • Edit Online

Almacenamiento en caché en memoria


Trabajar con una memoria caché distribuida
Detectar cambios con tokens de cambio
Almacenamiento en caché de respuestas
Middleware de almacenamiento en caché de respuestas
Aplicación auxiliar de etiquetas de caché
Aplicación auxiliar de etiquetas de caché distribuida
Almacenar en memoria caché en memoria en el
núcleo de ASP.NET
06/06/2018 • 9 minutes to read • Edit Online

Por Rick Anderson, John Luo, y Steve Smith


Vea o descargue el código de ejemplo (cómo descargarlo)

Conceptos básicos sobre el almacenamiento en caché


Almacenamiento en caché puede mejorar significativamente el rendimiento y la escalabilidad de una aplicación
al reducir el trabajo necesario para generar el contenido. Almacenamiento en caché funciona mejor con datos
que cambian con poca frecuencia. Almacenamiento en caché realiza una copia de datos que se pueden
devolver mucho más rápida que la de la fuente original. Debe escribir y probar la aplicación para que nunca
dependen de datos almacenados en caché.
ASP.NET Core es compatible con varias memorias caché diferentes. La memoria caché más sencilla se basa en
el IMemoryCache, que representa una caché almacenada en la memoria del servidor web. Las aplicaciones que
se ejecutan en una granja de servidores de varios servidores deben asegurarse de que las sesiones son rápidas
cuando se usa la memoria caché en memoria. Sesiones permanentes Asegúrese de que las solicitudes
posteriores de un cliente todos los vayan al mismo servidor. Por ejemplo, el uso de aplicaciones Web de Azure
enrutamiento de solicitud de aplicación (ARR ) para enrutar las solicitudes subsiguientes en el mismo servidor.
Las sesiones no son permanentes en una granja de servidores web requieren un caché distribuida para evitar
problemas de coherencia de la memoria caché. Para algunas aplicaciones, una memoria caché distribuida
puede admitir una escala mayor espera que una caché en memoria. Uso de una memoria caché distribuida,
descarga la memoria caché para un proceso externo.
El IMemoryCache caché expulsará las entradas de caché bajo presión de memoria, a menos que la almacenar en
caché prioridad está establecido en CacheItemPriority.NeverRemove . Puede establecer el CacheItemPriority
para ajustar la prioridad con la que la memoria caché extrae elementos bajo presión de memoria.
La memoria caché en memoria puede almacenar cualquier objeto; la interfaz de caché distribuida se limita a
byte[] .

Usar IMemoryCache
Almacenamiento en caché en memoria es un servicio que se hace referencia desde la aplicación con inyección
de dependencia. Llame a AddMemoryCache en ConfigureServices :
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;

public class Startup


{
public void ConfigureServices(IServiceCollection services)
{
services.AddMemoryCache();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}

public void Configure(IApplicationBuilder app)


{
app.UseMvcWithDefaultRoute();
}
}

Solicitar la IMemoryCache instancia en el constructor:

public class HomeController : Controller


{
private IMemoryCache _cache;

public HomeController(IMemoryCache memoryCache)


{
_cache = memoryCache;
}

IMemoryCache requiere el paquete NuGet Microsoft.Extensions.Caching.Memory.


IMemoryCache requiere el paquete NuGet Microsoft.Extensions.Caching.Memory, que está disponible en la
Microsoft.AspNetCore.All metapackage.
IMemoryCache requiere el paquete NuGet Microsoft.Extensions.Caching.Memory, que está disponible en la
Microsoft.AspNetCore.App metapackage.
El siguiente código utiliza TryGetValue para comprobar si es una hora en la memoria caché. Si no está
almacenado en caché una vez, se crea y se agrega a la caché con una nueva entrada establecer.

public IActionResult CacheTryGetValueSet()


{
DateTime cacheEntry;

// Look for cache key.


if (!_cache.TryGetValue(CacheKeys.Entry, out cacheEntry))
{
// Key not in cache, so get data.
cacheEntry = DateTime.Now;

// Set cache options.


var cacheEntryOptions = new MemoryCacheEntryOptions()
// Keep in cache for this time, reset time if accessed.
.SetSlidingExpiration(TimeSpan.FromSeconds(3));

// Save data in cache.


_cache.Set(CacheKeys.Entry, cacheEntry, cacheEntryOptions);
}

return View("Cache", cacheEntry);


}
Se muestran la hora actual y el tiempo en caché:

@model DateTime?

<div>
<h2>Actions</h2>
<ul>
<li><a asp-controller="Home" asp-action="CacheTryGetValueSet">TryGetValue and Set</a></li>
<li><a asp-controller="Home" asp-action="CacheGet">Get</a></li>
<li><a asp-controller="Home" asp-action="CacheGetOrCreate">GetOrCreate</a></li>
<li><a asp-controller="Home" asp-action="CacheGetOrCreateAsync">GetOrCreateAsync</a></li>
<li><a asp-controller="Home" asp-action="CacheRemove">Remove</a></li>
</ul>
</div>

<h3>Current Time: @DateTime.Now.TimeOfDay.ToString()</h3>


<h3>Cached Time: @(Model == null ? "No cached entry found" : Model.Value.TimeOfDay.ToString())</h3>

Las almacenadas en caché DateTime valor permanece en la memoria caché mientras haya solicitudes dentro
del tiempo de espera (y ningún expulsión debido a la presión de memoria). La siguiente imagen muestra la
hora actual y una hora anterior recuperadas desde la caché:

El siguiente código utiliza GetOrCreate y GetOrCreateAsync en caché los datos.


public IActionResult CacheGetOrCreate()
{
var cacheEntry = _cache.GetOrCreate(CacheKeys.Entry, entry =>
{
entry.SlidingExpiration = TimeSpan.FromSeconds(3);
return DateTime.Now;
});

return View("Cache", cacheEntry);


}

public async Task<IActionResult> CacheGetOrCreateAsync()


{
var cacheEntry = await
_cache.GetOrCreateAsync(CacheKeys.Entry, entry =>
{
entry.SlidingExpiration = TimeSpan.FromSeconds(3);
return Task.FromResult(DateTime.Now);
});

return View("Cache", cacheEntry);


}

El código siguiente llama obtener para capturar el tiempo en caché:

public IActionResult CacheGet()


{
var cacheEntry = _cache.Get<DateTime?>(CacheKeys.Entry);
return View("Cache", cacheEntry);
}

Vea IMemoryCache métodos y CacheExtensions métodos para obtener una descripción de los métodos de la
memoria caché.

Usar MemoryCacheEntryOptions
El ejemplo siguiente:
Establece la hora de expiración absoluta. Esto es el tiempo máximo que puede almacenarse en caché la
entrada y evita que el elemento se conviertan en obsoletos cuando continuamente se renueva el plazo de
caducidad.
Establece un tiempo de expiración variable. Las solicitudes que tienen acceso a este elemento almacenado
en caché, restablecerán el reloj de expiración deslizante.
Establece la prioridad de la memoria caché en CacheItemPriority.NeverRemove .
Establece un PostEvictionDelegate al que se llama después de la entrada se expulsa de la memoria caché. La
devolución de llamada se ejecuta en un subproceso diferente del código que se quita el elemento de la
memoria caché.
public IActionResult CreateCallbackEntry()
{
var cacheEntryOptions = new MemoryCacheEntryOptions()
// Pin to cache.
.SetPriority(CacheItemPriority.NeverRemove)
// Add eviction callback
.RegisterPostEvictionCallback(callback: EvictionCallback, state: this);

_cache.Set(CacheKeys.CallbackEntry, DateTime.Now, cacheEntryOptions);

return RedirectToAction("GetCallbackEntry");
}

public IActionResult GetCallbackEntry()


{
return View("Callback", new CallbackViewModel
{
CachedTime = _cache.Get<DateTime?>(CacheKeys.CallbackEntry),
Message = _cache.Get<string>(CacheKeys.CallbackMessage)
});
}

public IActionResult RemoveCallbackEntry()


{
_cache.Remove(CacheKeys.CallbackEntry);
return RedirectToAction("GetCallbackEntry");
}

private static void EvictionCallback(object key, object value,


EvictionReason reason, object state)
{
var message = $"Entry was evicted. Reason: {reason}.";
((HomeController)state)._cache.Set(CacheKeys.CallbackMessage, message);
}

Dependencias de caché
El ejemplo siguiente muestra cómo expirar una entrada de caché si expira una entrada dependiente. Un
CancellationChangeToken se agrega al elemento almacenado en caché. Cuando Cancel se llama en el
CancellationTokenSource , ambas entradas de caché se desalojan.
public IActionResult CreateDependentEntries()
{
var cts = new CancellationTokenSource();
_cache.Set(CacheKeys.DependentCTS, cts);

using (var entry = _cache.CreateEntry(CacheKeys.Parent))


{
// expire this entry if the dependant entry expires.
entry.Value = DateTime.Now;
entry.RegisterPostEvictionCallback(DependentEvictionCallback, this);

_cache.Set(CacheKeys.Child,
DateTime.Now,
new CancellationChangeToken(cts.Token));
}

return RedirectToAction("GetDependentEntries");
}

public IActionResult GetDependentEntries()


{
return View("Dependent", new DependentViewModel
{
ParentCachedTime = _cache.Get<DateTime?>(CacheKeys.Parent),
ChildCachedTime = _cache.Get<DateTime?>(CacheKeys.Child),
Message = _cache.Get<string>(CacheKeys.DependentMessage)
});
}

public IActionResult RemoveChildEntry()


{
_cache.Get<CancellationTokenSource>(CacheKeys.DependentCTS).Cancel();
return RedirectToAction("GetDependentEntries");
}

private static void DependentEvictionCallback(object key, object value,


EvictionReason reason, object state)
{
var message = $"Parent entry was evicted. Reason: {reason}.";
((HomeController)state)._cache.Set(CacheKeys.DependentMessage, message);
}

Mediante un CancellationTokenSource permite varias entradas de caché que se expulsen como un grupo. Con
el using patrón en el código anterior, las entradas de caché se crean dentro de la using bloque heredarán
opciones de expiración y desencadenadores.

Notas adicionales
Cuando se usa una devolución de llamada para rellenar un elemento de caché:
Varias solicitudes pueden encontrar el valor de clave almacenada en caché vacía porque no se ha
completado la devolución de llamada.
Esto puede dar lugar a que varios subprocesos al rellenar el elemento en caché.
Cuando una entrada de caché se utiliza para crear otro, el elemento secundario copia tokens de
expiración y la configuración de expiración basada en el momento de la entrada primaria. El elemento
secundario no está expirada para eliminación manual o actualización de la entrada primaria.

Recursos adicionales
Trabajar con una memoria caché distribuida
Detectar cambios con tokens de cambio
Almacenamiento en caché de respuestas
Middleware de almacenamiento en caché de respuestas
Aplicación auxiliar de etiquetas de caché
Aplicación auxiliar de etiquetas de caché distribuida
Trabajar con una memoria caché distribuida en
ASP.NET Core
06/06/2018 • 11 minutes to read • Edit Online

Por Steve Smith


Las memorias caché distribuidas pueden mejorar el rendimiento y la escalabilidad de las aplicaciones de
ASP.NET Core, especialmente cuando se hospeda en un entorno de granja de servidores en la nube o de
servidor. Este artículo explica cómo trabajar con implementaciones y abstracciones de caché distribuida
integrado del núcleo de ASP.NET.
Vea o descargue el código de ejemplo (cómo descargarlo)

¿Qué es una memoria caché distribuida


Una memoria caché distribuida es compartida por varios servidores de aplicación (consulte conceptos básicos
de la memoria caché). La información de la memoria caché no se guarda en la memoria de los servidores web
individuales y los datos almacenados en caché están disponibles para todos los servidores de aplicaciones.
Esto proporciona varias ventajas:
1. Los datos almacenados en caché son coherentes en todos los servidores web. Los usuarios no verán
resultados distintos sin importar el servidor web que controla su solicitud.
2. Los datos almacenados en caché sobreviven a reinicios del servidor web y las implementaciones.
Servidores web individuales se pueden quitar o agregar sin afectar a la memoria caché.
3. El almacén de datos de origen tiene menos las solicitudes realizadas a él (de con varias cachés en
memoria o en no caché en absoluto).

NOTE
Si usa una memoria caché distribuida de SQL Server, algunas de las siguientes ventajas solo son ciertas si se utiliza una
instancia de base de datos independiente para la memoria caché que para los datos de origen de la aplicación.

Al igual que cualquier memoria caché, una memoria caché distribuida puede mejorar considerablemente la
capacidad de respuesta de una aplicación, ya que normalmente se pueden recuperar datos de la memoria
caché mucho más rápida que de una base de datos relacional (o servicio web).
La configuración de la caché es específica de la implementación. En este artículo se describe cómo configurar
los almacenes distribuidos en caché Redis y SQL Server. Independientemente de qué implementación se
seleccione, la aplicación interactúa con la memoria caché utilizando una interfaz común IDistributedCache .

La interfaz de IDistributedCache
El IDistributedCache interfaz incluye métodos sincrónicos y asincrónicos. La interfaz permite elementos
agregar, recuperar y quitar de la implementación de caché distribuida. El IDistributedCache interfaz incluye los
métodos siguientes:
Get, GetAsync
Toma una clave de cadena y recupera un elemento almacenado en caché como un byte[] si se encuentra en la
memoria caché.
Conjunto, SetAsync
Agrega un elemento (como byte[] ) a la memoria caché utilizando una clave de cadena.
Actualización de RefreshAsync
Actualiza un elemento en la memoria caché basado en su clave, restablecer su tiempo de espera de expiración
deslizante (si existe).
Quitar, aplica removeasync a
Quita una entrada de caché basada en su clave.
Para usar el IDistributedCache interfaz:
1. Agregue los paquetes de NuGet necesarios para el archivo de proyecto.
2. Configurar la implementación específica de IDistributedCache en su Startup la clase
ConfigureServices método y lo agrega al contenedor no existe.

3. Desde la aplicación Middleware o las clases de controlador MVC, solicite una instancia de
IDistributedCache desde el constructor. La instancia se proporcionarán con inyección de dependencia
(DI).

NOTE
No es necesario usar una duración de Singleton o en el ámbito para IDistributedCache instancias (al menos para las
implementaciones integradas). También puede crear una instancia donde podría necesitar uno (en lugar de usar inyección
de dependencia), pero esto puede dificultar el código probar e infringe el principio de dependencias explícitas.

En el ejemplo siguiente se muestra cómo utilizar una instancia de IDistributedCache en un componente de


middleware simple:
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Distributed;

namespace DistCacheSample
{
public class StartTimeHeader
{
private readonly RequestDelegate _next;
private readonly IDistributedCache _cache;

public StartTimeHeader(RequestDelegate next,


IDistributedCache cache)
{
_next = next;
_cache = cache;
}

public async Task Invoke(HttpContext httpContext)


{
string startTimeString = "Not found.";
var value = await _cache.GetAsync("lastServerStartTime");
if (value != null)
{
startTimeString = Encoding.UTF8.GetString(value);
}

httpContext.Response.Headers.Append(
"Last-Server-Start-Time", startTimeString);

await _next.Invoke(httpContext);
}
}

// Add the middleware to the HTTP request pipeline.


public static class StartTimeHeaderExtensions
{
public static IApplicationBuilder UseStartTimeHeader(
this IApplicationBuilder builder)
{
return builder.UseMiddleware<StartTimeHeader>();
}
}
}

En el código anterior, el valor almacenado en caché se lee pero nunca se ha escrito. En este ejemplo, el valor
sólo se establece cuando se inicia un servidor y no cambia. En un escenario de varios servidores, el servidor
más reciente para iniciar sobrescribirá los valores anteriores que se establecieron con otros servidores. El Get
y Set métodos utilizan el byte[] tipo. Por lo tanto, se debe convertir el valor de cadena mediante
Encoding.UTF8.GetString (para Get ) y Encoding.UTF8.GetBytes (para Set ).

El siguiente código Startup.cs muestra el valor que se va a establecer:


public void Configure(IApplicationBuilder app,
IDistributedCache cache)
{
var serverStartTimeString = DateTime.Now.ToString();
byte[] val = Encoding.UTF8.GetBytes(serverStartTimeString);
var cacheEntryOptions = new DistributedCacheEntryOptions()
.SetSlidingExpiration(TimeSpan.FromSeconds(30));
cache.Set("lastServerStartTime", val, cacheEntryOptions);

NOTE
Puesto que IDistributedCacheestá configurado en el ConfigureServices método, está disponible para el
Configure método como parámetro. Agregado como un parámetro permitirá proporcionar la instancia configurada a
través de DI.

Uso de una caché de Redis distribuida


Redis es un almacén de datos en memoria de código abierto, que a menudo se usa como una memoria caché
distribuida. Puede usar de forma local, y puede configurar un Azure Redis Cache para las aplicaciones
principales de ASP.NET hospedados en Azure. La aplicación de ASP.NET Core configura la implementación de
caché mediante un RedisDistributedCache instancia.
Configurar la implementación de Redis en ConfigureServices y tener acceso a él en el código de aplicación
solicitando una instancia de IDistributedCache (vea el código anterior).
En el código de ejemplo, un RedisCache implementación se utiliza cuando el servidor está configurado para un
Staging entorno. Por lo tanto la ConfigureStagingServices método configura el RedisCache :

public void ConfigureStagingServices(IServiceCollection services)


{
services.AddDistributedRedisCache(options =>
{
options.Configuration = "localhost";
options.InstanceName = "SampleInstance";
});
}

NOTE
Para instalar Redis en el equipo local, instale el paquete chocolatey https://chocolatey.org/packages/redis-64/ y ejecutar
redis-server desde un símbolo del sistema.

Uso de un servidor SQL de caché distribuida


La implementación de SqlServerCache permite que la memoria caché distribuida usar una base de datos de
SQL Server como su memoria auxiliar. Para crear SQL Server se puede utilizar la herramienta de caché de sql,
la herramienta crea una tabla con el nombre y el esquema que especifica.
Agregar SqlConfig.Tools a la <ItemGroup> elemento del archivo de proyecto y ejecutar dotnet restore .

<ItemGroup>
<DotNetCliToolReference Include="Microsoft.Extensions.Caching.SqlConfig.Tools"
Version="2.0.2" />
</ItemGroup>
Probar SqlConfig.Tools, ejecute el siguiente comando:

dotnet sql-cache create --help

SqlConfig.Tools muestra el uso, opciones y ayuda del comando.


Crear una tabla de SQL Server mediante la ejecución de la sql-cache create comando:

dotnet sql-cache create "Data Source=(localdb)\v11.0;Initial Catalog=DistCache;Integrated Security=True;"


dbo TestCache
info: Microsoft.Extensions.Caching.SqlConfig.Tools.Program[0]
Table and index were created successfully.

La tabla creada tiene el siguiente esquema:

Al igual que todas las implementaciones de caché, la aplicación debe obtener y establecer valores de caché
mediante una instancia de IDistributedCache , no un SqlServerCache . El ejemplo implementa SqlServerCache
en el entorno de producción (por lo que se configura en ConfigureProductionServices ).

public void ConfigureProductionServices(IServiceCollection services)


{
services.AddDistributedSqlServerCache(options =>
{
options.ConnectionString =
@"Data Source=(localdb)\v11.0;Initial Catalog=DistCache;" +
@"Integrated Security=True;";
options.SchemaName = "dbo";
options.TableName = "TestCache";
});
}

NOTE
El ConnectionString (y, opcionalmente, SchemaName y TableName ) normalmente deberían almacenarse fuera de
control de código fuente (por ejemplo, UserSecrets), ya que pueden contener las credenciales.

Recomendaciones
La hora de decidir qué implementación de IDistributedCache es adecuado para la aplicación, elija entre Redis
y SQL Server se basa en la infraestructura existente y entorno, los requisitos de rendimiento y experiencia de
su equipo. Si su equipo es más fácil trabajar con Redis, es una excelente opción. Si el equipo prefiera SQL
Server, puede estar seguro de que así la implementación. Tenga en cuenta que una solución de
almacenamiento en caché tradicional almacena datos en memoria que permite la recuperación rápida de datos.
Debe almacenar los datos de uso frecuente en una memoria caché y almacenar todos los datos en un almacén
persistente de back-end como SQL Server o el almacenamiento de Azure. Caché en Redis es una solución de
almacenamiento en caché que proporciona un alto rendimiento y baja latencia en comparación con la memoria
caché de SQL.
Recursos adicionales
Caché en Azure en Redis
Base de datos SQL en Azure
Almacenamiento en caché en memoria
Detectar cambios con tokens de cambio
Almacenamiento en caché de respuestas
Middleware de almacenamiento en caché de respuestas
Aplicación auxiliar de etiquetas de caché
Aplicación auxiliar de etiquetas de caché distribuida
Las respuestas en caché de ASP.NET Core
06/06/2018 • 16 minutes to read • Edit Online

Por John Luo, Rick Anderson, Steve Smith, y Luke Latham

NOTE
Las respuestas en caché en las páginas de Razor está disponible en ASP.NET Core 2.1 o posterior.

Vea o descargue el código de ejemplo (cómo descargarlo)


Las respuestas en caché reducen el número de solicitudes de que un cliente o proxy se realiza en un servidor
web. Las respuestas en caché también reducen la cantidad de trabajo realiza el servidor web para generar una
respuesta. Las respuestas en caché se controlan mediante encabezados que especifican cómo desea que cliente,
el proxy y middleware para la memoria caché las respuestas.
El servidor web puede almacenar en caché las respuestas al agregar Middleware de almacenamiento en caché de
respuesta.

Almacenamiento en caché de respuesta basado en HTTP


El especificación HTTP 1.1 Caching describe el comportamiento de las cachés de Internet. El encabezado HTTP
principal que se usa para almacenar en caché es Cache-Control, que se utiliza para especificar la caché directivas.
Las directivas de controlan el comportamiento de almacenamiento en caché las solicitudes lleguen su desde los
clientes a los servidores a medida que y respuestas lleguen su de servidores a los clientes. Mover las solicitudes y
respuestas a través de servidores proxy y servidores proxy deben también ajustarse a la especificación HTTP 1.1
almacenamiento en memoria caché.
Common Cache-Control directivas se muestran en la tabla siguiente.

DIRECTIVA ACCIÓN

public Una memoria caché puede almacenar la respuesta.

private No se debe almacenar la respuesta de una memoria caché


compartida. Una caché privada puede almacenar y reutilizar
la respuesta.

max-age El cliente no aceptará una respuesta cuya antigüedad es


superior al número especificado de segundos. Ejemplos:
max-age=60 (60 segundos), max-age=2592000 (1 mes)

sin caché En las solicitudes: una memoria caché no debe usar una
respuesta almacenada para satisfacer la solicitud. Nota: El
servidor de origen vuelve a genera la respuesta para el
cliente y el software intermedio actualiza la respuesta
almacenada en la memoria caché.

En las respuestas: la respuesta no debe usarse para una


solicitud posterior sin la validación en el servidor de origen.
DIRECTIVA ACCIÓN

ningún almacén En las solicitudes: una memoria caché no debe almacenar la


solicitud.

En las respuestas: una memoria caché no debe almacenar


cualquier parte de la respuesta.

Otros encabezados de caché que desempeñan un papel en el almacenamiento en caché se muestran en la tabla
siguiente.

HEADER FUNCIÓN

Edad Una estimación de la cantidad de tiempo en segundos desde


que se generó la respuesta o se ha validado correctamente
en el servidor de origen.

Expira La fecha y hora después de que la respuesta se considera


obsoleta.

Pragma Existe para hacia atrás compatibilidad con HTTP/1.0 almacena


en memoria caché de configuración no-cache
comportamiento. Si el Cache-Control encabezado está
presente, el Pragma se omite el encabezado.

Variar Especifica que una respuesta almacenada en caché no se


debe enviar a menos que todos los de la Vary coinciden
con campos de encabezado de solicitud original de la
respuesta almacenada en caché y la solicitud nuevo.

Las directivas de Control de caché de solicitud de aspectos de


almacenamiento en caché basado en HTTP
El especificación HTTP 1.1 almacenamiento en memoria caché para el encabezado Cache-Control requiere una
memoria caché que se respeten válido Cache-Control encabezado enviado por el cliente. Un cliente puede
realizar las solicitudes con un no-cache valor del encabezado y se fuerza el servidor para generar una nueva
respuesta para cada solicitud.
Siempre respetando cliente Cache-Control encabezados de solicitud tiene sentido si considera que el objetivo del
almacenamiento en caché de HTTP. En la especificación oficial, el almacenamiento en caché está pensado para
reducir la sobrecarga de red y latencia de satisfacer las solicitudes a través de una red de clientes, servidores
proxy y servidores. No es necesariamente una manera de controlar la carga en un servidor de origen.
No hay ningún control del desarrollador actual sobre este comportamiento de almacenamiento en caché cuando
se usa el Middleware de almacenamiento en caché de respuesta porque el middleware se adhiere a los oficiales
especificación el almacenamiento en caché. Mejoras futuras en el middleware permitirá configurar el middleware
para omitir una solicitud Cache-Control encabezado al decidir atender una respuesta almacenada en caché. Esto
le ofrece la oportunidad para controlar mejor la carga en el servidor cuando se usa el middleware.

Otras tecnologías de almacenamiento en caché de ASP.NET Core


Almacenamiento en caché en memoria
Almacenamiento en caché en memoria utiliza memoria del servidor para almacenar los datos almacenados en
caché. Este tipo de almacenamiento en caché es adecuado para un único servidor o varios servidores mediante
sesiones permanentes. Sesiones permanentes significa que siempre se enrutan las solicitudes de un cliente en el
mismo servidor para su procesamiento.
Para obtener más información, consulte almacenar en memoria caché en memoria.
Caché distribuida
Usar una memoria caché distribuida para almacenar datos en la memoria cuando la aplicación se hospeda en
una granja de servidores en la nube o el servidor. La memoria caché se comparte entre los servidores que
procesan las solicitudes. Un cliente puede enviar una solicitud que controla cualquier servidor en el grupo si los
datos almacenados en caché para el cliente están disponibles. ASP.NET Core ofrece SQL Server y las cachés de
Redis distribuida.
Para obtener más información, consulte trabajar con una memoria caché distribuida.
Aplicación auxiliar de etiqueta de caché
Puede almacenar en caché el contenido de una vista MVC o Razor página a la aplicación auxiliar de etiqueta de
caché. La aplicación auxiliar de etiqueta de caché usa almacenamiento en caché en memoria para almacenar
datos.
Para obtener más información, consulte aplicación auxiliar de la etiqueta de caché en MVC de ASP.NET Core.
Aplicación auxiliar de etiquetas de caché distribuida
Puede almacenar en caché el contenido de una vista MVC o la página de Razor en nube distribuida o escenarios
de granja de servidores web con el Ayudante de etiqueta de caché distribuida. El Ayudante de etiqueta de caché
distribuida usa SQL Server o Redis para almacenar datos.
Para obtener más información, consulte auxiliar de etiqueta de caché distribuida.

Atributo ResponseCache
El ResponseCacheAttribute especifica los parámetros necesarios para establecer encabezados apropiados en las
respuestas en caché.

WARNING
Deshabilitar el almacenamiento en caché para el contenido que contiene información para clientes autenticados. Sólo debe
habilitarse el almacenamiento en caché para el contenido que no cambia en función de la identidad de un usuario o si un
usuario ha iniciado sesión.

VaryByQueryKeys varía la respuesta almacenada en los valores de la lista de claves de consulta determinada.
Cuando un valor único de * es siempre el middleware varía las respuestas por todos los parámetros de cadena
de consulta de solicitud. VaryByQueryKeys requiere ASP.NET Core 1.1 o posterior.
El Middleware de almacenamiento en caché de la respuesta debe estar habilitado para establecer el
VaryByQueryKeys propiedad; en caso contrario, se produce una excepción en tiempo de ejecución. No hay un
encabezado HTTP correspondiente para el VaryByQueryKeys propiedad. La propiedad es una característica HTTP
controlada el Middleware de almacenamiento en caché de respuesta. Para que el middleware atender una
respuesta almacenada en caché, la cadena de consulta y el valor de cadena de consulta deben coincidir con una
solicitud anterior. Por ejemplo, considere la posibilidad de la secuencia de las solicitudes y resultados que se
muestran en la tabla siguiente.

SOLICITUD RESULTADO

http://example.com?key1=value1 devuelta por el servidor


SOLICITUD RESULTADO

http://example.com?key1=value1 procedentes de middleware

http://example.com?key1=value2 devuelta por el servidor

La primera solicitud es devuelto por el servidor y en memoria caché de middleware. Dado que la cadena de
consulta coincide con la solicitud anterior, se devuelve la segunda solicitud middleware. La tercera solicitud no se
encuentra en la caché de middleware porque el valor de cadena de consulta no coincide con una solicitud
anterior.
El ResponseCacheAttribute se utiliza para crear y configurar (a través de IFilterFactory ) un
ResponseCacheFilter. El ResponseCacheFilter realiza el trabajo de actualización de los encabezados HTTP
adecuados y características de la respuesta. El filtro:
Quita los encabezados existentes para Vary , Cache-Control , y Pragma .
Escribe los encabezados adecuados en función de las propiedades establecidas en el ResponseCacheAttribute .
Actualiza la respuesta si el almacenamiento en caché característica HTTP VaryByQueryKeys se establece.
Variar
Este encabezado solo cuando se escribe el VaryByHeader se establece la propiedad. Se establece en el Vary valor
de la propiedad. El siguiente ejemplo se utiliza la VaryByHeader propiedad:
[!code-csharp]
[!code-csharp]
Puede ver los encabezados de respuesta con las herramientas de red de su explorador. La siguiente imagen
muestra la F12 de borde de salida en el red ficha cuando el About2 se actualiza el método de acción:

NoStore y Location.None
NoStore invalida la mayoría de las demás propiedades. Cuando esta propiedad se establece en true ,
Cache-Control encabezado se establece en no-store . Si Location está establecido en None :
El valor de Cache-Control está establecido en no-store,no-cache .
El valor de Pragma está establecido en no-cache .

Si NoStore es false y Location es None , Cache-Control y Pragma se establecen en no-cache .


Normalmente establece NoStore a true en las páginas de error. Por ejemplo:
[!code-csharp]
[!code-csharp]
Esto genera los encabezados siguientes:

Cache-Control: no-store,no-cache
Pragma: no-cache

Ubicación y la duración
Para habilitar el almacenamiento en caché, Duration debe establecerse en un valor positivo y Location debe ser
Any (valor predeterminado) o Client . En este caso, el Cache-Control encabezado se establece en el valor de
ubicación seguido por el max-age de la respuesta.

NOTE
Location de opciones de Any y Client traducir Cache-Control valores de encabezado public y private ,
respectivamente. Como se indicó anteriormente, establecer Location a None establece ambas Cache-Control y
Pragma encabezados para no-cache .

A continuación se muestra un ejemplo que muestra los encabezados genera estableciendo Duration y deja el
valor predeterminado Location valor:
[!code-csharp]
[!code-csharp]
Esto produce el siguiente encabezado:

Cache-Control: public,max-age=60

Perfiles de memoria caché


En lugar de duplicar ResponseCache configuración en muchos atributos de acción de controlador, perfiles de
memoria caché se puede configurar como opciones al configurar MVC en la ConfigureServices método
Startup . Valores que se encuentran en un perfil de caché que se hace referencia se utilizan como los valores
predeterminados por el ResponseCache de atributo y se reemplazan por las propiedades especificadas en el
atributo.
Cómo configurar un perfil de caché:
[!code-csharp]
[!code-csharp]
Hacer referencia a un perfil de caché:
[!code-csharp]
[!code-csharp]
El ResponseCache atributo se puede aplicar tanto a las acciones (métodos) y controladores (clases). Atributos de
nivel de método invalidan la configuración especificada en los atributos de nivel de clase.
En el ejemplo anterior, un atributo de nivel de clase especifica una duración de 30 segundos, mientras que un
atributo de nivel de método hace referencia a un perfil de caché con una duración establecida en 60 segundos.
El encabezado resultante:

Cache-Control: public,max-age=60

Recursos adicionales
Almacenar las respuestas en las cachés
Control de caché
Almacenamiento en caché en memoria
Trabajar con una memoria caché distribuida
Detectar cambios con tokens de cambio
Middleware de almacenamiento en caché de respuestas
Aplicación auxiliar de etiquetas de caché
Aplicación auxiliar de etiquetas de caché distribuida
Respuesta de almacenamiento en caché de
Middleware en ASP.NET Core
14/06/2018 • 14 minutes to read • Edit Online

Por Luke Latham y John Luo


Ver o descargar el código de ejemplo de ASP.NET Core 2.1 (cómo descargar)
En este artículo se explica cómo configurar el Middleware de almacenamiento en caché de respuesta en una
aplicación de ASP.NET Core. El middleware determina cuando las respuestas son almacenable en caché, las
respuestas de los almacenes y actúa las respuestas de caché. Para obtener una introducción al almacenamiento
en caché de HTTP y el ResponseCache de atributo, vea las respuestas en caché.

Package
Para incluir el software intermedio en el proyecto, agregue una referencia a la
Microsoft.AspNetCore.ResponseCaching empaquetar o usar el Microsoft.AspNetCore.App metapackage, que
está disponible para su uso en ASP. Núcleo NET 2.1 o posterior.

Configuración
En ConfigureServices , agregue el middleware a la colección de servicio.

public void ConfigureServices(IServiceCollection services)


{
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});

services.AddResponseCaching();
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}

Configurar la aplicación para usar el middleware con el UseResponseCaching método de extensión, que agrega
el middleware a la canalización de procesamiento de la solicitud. La aplicación de ejemplo agrega un
Cache-Control encabezado a la respuesta que se almacena en caché las respuestas almacenable en caché
durante 10 segundos. El ejemplo envía una Vary encabezado para configurar el middleware para dar servicio
a un respuesta almacenada en caché solo si la Accept-Encoding coincide con el encabezado de las solicitudes
posteriores de la solicitud original. En el ejemplo de código siguiente, CacheControlHeaderValue y
HeaderNames requieren un using instrucción para el Microsoft.Net.Http.Headers espacio de nombres.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();

app.UseResponseCaching();

app.Use(async (context, next) =>


{
context.Response.GetTypedHeaders().CacheControl =
new Microsoft.Net.Http.Headers.CacheControlHeaderValue()
{
Public = true,
MaxAge = TimeSpan.FromSeconds(10)
};
context.Response.Headers[Microsoft.Net.Http.Headers.HeaderNames.Vary] =
new string[] { "Accept-Encoding" };

await next();
});

app.UseMvc();
}

Middleware de almacenamiento en caché de respuesta sólo almacena en caché las respuestas del servidor que
dan como resultado un código de estado 200 (OK). Otras respuestas, incluidos los páginas de errores, se
omiten el middleware.

WARNING
Las respuestas que incluye contenido para clientes autenticados deben marcarse como no almacenable en caché para
evitar que el middleware de almacenamiento y envío de las respuestas. Vea condiciones para almacenar en caché para
obtener más información acerca de cómo el middleware determina si es almacenable en caché una respuesta.

Opciones
El middleware ofrece tres opciones para controlar las respuestas en caché.

OPCIÓN DESCRIPCIÓN

UseCaseSensitivePaths Determina si se almacenan en caché las respuestas en las


rutas de acceso entre mayúsculas y minúsculas. El valor
predeterminado es false .

MaximumBodySize El mayor tamaño almacenable en caché para el cuerpo de


respuesta en bytes. El valor predeterminado es
64 * 1024 * 1024 (64 MB).
OPCIÓN DESCRIPCIÓN

SizeLimit El límite de tamaño para el middleware de caché de


respuesta en bytes. El valor predeterminado es
100 * 1024 * 1024 (100 MB).

En el ejemplo siguiente se configura el middleware de:


Almacenar en caché las respuestas menores o iguales a 1.024 bytes.
Almacene las respuestas por las rutas de acceso entre mayúsculas y minúsculas (por ejemplo, /page1 y
/Page1 se almacenan por separado).

services.AddResponseCaching(options =>
{
options.UseCaseSensitivePaths = true;
options.MaximumBodySize = 1024;
});

VaryByQueryKeys
Al utilizar controladores de MVC o Web API o modelos de página de las páginas de Razor, el ResponseCache
atributo especifica los parámetros necesarios para establecer los encabezados adecuados para las respuestas
en caché. El único parámetro de la ResponseCache atributo que estrictamente requiere el software intermedio es
VaryByQueryKeys , que no se corresponde con un encabezado HTTP real. Para obtener más información,
consulte ResponseCache atributo.
Si no usa el ResponseCache las respuestas en caché pueden variar atributo, con el VaryByQueryKeys
característica. Use la ResponseCachingFeature directamente desde el IFeatureCollection de la HttpContext :

var responseCachingFeature = context.HttpContext.Features.Get<IResponseCachingFeature>();


if (responseCachingFeature != null)
{
responseCachingFeature.VaryByQueryKeys = new[] { "MyKey" };
}

Usa un solo valor igual a * en VaryByQueryKeys varía en la memoria caché por todos los parámetros de
consulta de solicitud.

Encabezados HTTP utilizadas por el Middleware de almacenamiento


en caché de respuesta
Las respuestas en caché el middleware se configura mediante encabezados HTTP.

HEADER DETALLES

Autorización La respuesta no está almacenado en memoria caché si el


encabezado no existe.
HEADER DETALLES

Control de caché El middleware solamente tiene en cuenta el almacenamiento


en caché las respuestas que se marca con el public
directiva de caché. Controlar el almacenamiento en caché
con los parámetros siguientes:
max-age
Max obsoleta†
min-nuevo
Directiva must-revalidate
sin caché
ningún almacén
solo if-almacenamiento en caché
private
public
s-maxage
proxy-revalidate‡
†Si no se especifica ningún límite para max-stale , el
middleware no realiza ninguna acción.
‡ proxy-revalidate tiene el mismo efecto que
must-revalidate .

Para obtener más información, consulte RFC 7231: solicitar


directivas de Cache-Control.

Pragma A Pragma: no-cache encabezado en la solicitud produce el


mismo efecto que Cache-Control: no-cache . Este
encabezado se haya reemplazado por las directivas
correspondientes en el Cache-Control encabezado, si está
presente. Cuenta para mantener la compatibilidad con
HTTP/1.0.

Set-Cookie La respuesta no está almacenado en memoria caché si el


encabezado no existe. Cualquier middleware en la
canalización de procesamiento de la solicitud que establece
una o más cookies impide que el Middleware de
almacenamiento en caché de respuesta de almacenamiento
en caché la respuesta (por ejemplo, el basado en cookies
proveedor TempData).

Variar El Vary encabezado se utiliza para modificar la respuesta


almacenada en caché por otro encabezado. Por ejemplo,
almacenar en caché las respuestas mediante la codificación
mediante la inclusión de la Vary: Accept-Encoding
encabezado, que se almacena en caché las respuestas para
las solicitudes con encabezados Accept-Encoding: gzip y
Accept-Encoding: text/plain por separado. Una
respuesta con un valor de encabezado de * nunca se
almacena.

Expires Una respuesta considera obsoleta por este encabezado no


almacenan o recuperan a menos que se reemplaza por otro
Cache-Control encabezados.

If-None-Match La respuesta completa se sirve desde la memoria caché si el


valor no es * y ETag de la respuesta no coincide con
ninguno de los valores proporcionados. En caso contrario, se
otorga una respuesta 304 (no modificado).
HEADER DETALLES

If-Modified-Since Si el If-None-Match encabezado no está presente, una


respuesta completa se sirve desde la memoria caché si la
fecha de la respuesta almacenada en caché es más reciente
que el valor proporcionado. En caso contrario, se otorga una
respuesta 304 (no modificado).

Fecha Cuando se trabaja desde la memoria caché, el Date


encabezado será ajustado por el middleware si no se
proporcionó en la respuesta original.

Longitud del contenido Cuando se trabaja desde la memoria caché, el


Content-Length encabezado será ajustado por el
middleware si no se proporcionó en la respuesta original.

Edad El Age encabezado enviado en la respuesta original se


omite. El middleware calcula un nuevo valor al ofrecer
servicio a una respuesta almacenada en caché.

Almacenamiento en caché respeta las directivas de solicitud Cache-


Control
El middleware respeta las reglas de la especificación HTTP 1.1 Caching. Las reglas requieren una memoria
caché que se respeten válido Cache-Control encabezado enviado por el cliente. En la especificación, un cliente
puede realizar las solicitudes con un no-cache valor del encabezado y se fuerza el servidor para generar una
nueva respuesta para cada solicitud. Actualmente no hay ningún control del desarrollador sobre este
comportamiento de almacenamiento en caché cuando se usa el middleware porque el middleware se adhiere a
la especificación oficial de almacenamiento en caché.
Para tener más control sobre el almacenamiento en caché de comportamiento, explore otras características de
almacenamiento en caché de ASP.NET Core. Consulte los temas siguientes:
Almacenamiento en caché en memoria
Trabajar con una memoria caché distribuida
Almacenar en caché auxiliar de etiqueta en el núcleo de ASP.NET MVC
Aplicación auxiliar de etiquetas de caché distribuida

Solución de problemas
Si almacenamiento en caché de comportamiento no es como se esperaba, confirme que las respuestas son
almacenable en caché y puede que se va a obtener de la caché. Examine los encabezados de la solicitud
entrante y encabezados de salida de la respuesta. Habilitar registro para ayudar con la depuración.
Al probar y solucionar problemas de comportamiento de almacenamiento en caché, un explorador puede
establecer encabezados de solicitud que afectan al almacenamiento en caché de maneras no deseados. Por
ejemplo, puede establecer un explorador la Cache-Control encabezado a no-cache o max-age=0 al actualizar
una página. Las siguientes herramientas pueden establecer explícitamente los encabezados de solicitud y son
preferibles para las pruebas de almacenamiento en caché:
Fiddler
Postman
Condiciones para el almacenamiento en caché
La solicitud debe producir una respuesta del servidor con un código de estado 200 (OK).
El método de solicitud debe ser GET o HEAD.
Middleware de Terminal, como Middleware de archivos estáticos, no se debe procesar la respuesta antes de
Middleware de almacenamiento en caché de respuesta.
El Authorization encabezado no debe estar presente.
Cache-Control parámetros del encabezado deben ser válidos y debe marcarse la respuesta public y no
marcado como private .
El Pragma: no-cache encabezado no debe estar presente si el Cache-Control encabezado no está presente,
como la Cache-Control encabezado reemplaza la Pragma encabezado cuando está presente.
El Set-Cookie encabezado no debe estar presente.
Vary parámetros del encabezado deben ser válida y no es igual a * .
La Content-Length valor de encabezado (si establece) debe coincidir con el tamaño del cuerpo de respuesta.
El IHttpSendFileFeature no se utiliza.
La respuesta no debe ser obsoleta según lo especificado por el Expires encabezado y el max-age y
s-maxage directivas de caché.
Búfer de respuesta debe ser correcta, y el tamaño de la respuesta debe ser menor que el configurado o
default SizeLimit .
La respuesta debe ser almacenable en caché según el RFC 7234 especificaciones. Por ejemplo, el no-store
directiva no debe existir en los campos de encabezado de solicitud o respuesta. Vea sección 3: almacenar
respuestas en las cachés de RFC 7234 para obtener más información.

NOTE
El sistema Antiforgery para generar tokens seguros para evitar la falsificación de solicitud entre sitios (CSRF) ataques
conjuntos el Cache-Control y Pragma encabezados a no-cache para que no se almacenan en caché las respuestas.
Para obtener información acerca de cómo deshabilitar antiforgery tokens para los elementos de formulario HTML, vea
antiforgery configuración de ASP.NET Core.

Recursos adicionales
Inicio de aplicaciones
Middleware
Almacenamiento en caché en memoria
Trabajar con una memoria caché distribuida
Detectar cambios con tokens de cambio
Almacenamiento en caché de respuestas
Aplicación auxiliar de etiquetas de caché
Aplicación auxiliar de etiquetas de caché distribuida
Middleware de compresión de respuesta para
ASP.NET Core
22/06/2018 • 18 minutes to read • Edit Online

Por Luke Latham


Vea o descargue el código de ejemplo (cómo descargarlo)
Ancho de banda de red es un recurso limitado. Reducir el tamaño de la respuesta normalmente aumenta la
capacidad de respuesta de una aplicación, a menudo drásticamente. Una manera de reducir el tamaño de carga es
comprimir las respuestas de una aplicación.

Cuándo utilizar Middleware de compresión de respuesta


Utilizar tecnologías de compresión de respuesta basada en servidor en IIS, Apache o Nginx. El rendimiento del
middleware de probablemente no coincida con el de los módulos del servidor. Servidor HTTP.sys y Kestrel
actualmente no ofrecen compatibilidad con la compresión integrada.
Use Middleware de compresión de respuesta cuando esté:
No se puede usar las siguientes tecnologías de compresión basada en servidor:
Módulo de compresión dinámica de IIS
Módulo de mod_deflate de Apache
Nginx compresión y descompresión
Hospeda directamente en:
Servidor HTTP.sys (anteriormente denominados WebListener)
Kestrel

Compresión de respuesta
Por lo general, cualquier respuesta que comprimen de forma nativa puede beneficiarse de compresión de
respuesta. Respuestas de forma no nativa comprimidas normalmente son: CSS, JavaScript, HTML, XML y JSON.
No debe comprimir activos comprimidos de forma nativa, como archivos PNG. Si se intentan volver a comprimir
aún más una respuesta cifrada de forma nativa, cualquier pequeña reducción adicional en tiempo de tamaño y la
transmisión probablemente resultar mínimo comparado con el tiempo que tardó en procesarse la compresión.
No comprimir los archivos de menos de aproximadamente 150-1000 bytes (según el contenido del archivo y la
eficacia de compresión). La sobrecarga de la compresión de archivos pequeños puede generar un archivo
comprimido mayor que el archivo sin comprimir.
Cuando un cliente puede procesar contenido comprimido, el cliente debe informar al servidor de sus capacidades
mediante el envío de la Accept-Encoding encabezado con la solicitud. Cuando un servidor envía contenido
comprimido, debe incluir información de la Content-Encoding encabezado en cómo se codifican las respuestas
comprimidas. Contenido designaciones de codificación admitidas por el middleware se muestran en la tabla
siguiente.

ACCEPT-ENCODING VALORES DE ENCABEZADO SOFTWARE INTERMEDIO COMPATIBLE DESCRIPCIÓN

br No Formato de los datos comprimidos de


Brotli
ACCEPT-ENCODING VALORES DE ENCABEZADO SOFTWARE INTERMEDIO COMPATIBLE DESCRIPCIÓN

compress No Formato de datos de "comprimir" de


UNIX

deflate No "deflate" datos comprimidos en el


formato de datos de "zlib"

exi No Intercambio eficaz de XML de W3C

gzip Sí (valor predeterminado) formato de archivo GZIP

identity Sí Identificador de "Sin codificación": no se


debe codificar la respuesta.

pack200-gzip No Formato de transferencia de red para


archivos de Java

* Sí Cualquier contenido disponible no


codificación explícitamente solicitada

Para obtener más información, consulte el lista de codificación oficial contenido de IANA.
El middleware le permite agregar proveedores de compresión adicional para personalizado Accept-Encoding
valores de encabezado. Para obtener más información, consulte proveedores personalizados a continuación.
El software intermedio es capaz de reaccionar ante el valor de calidad (qvalue, q ) ponderación cuando enviada
por el cliente para dar prioridad a los esquemas de compresión. Para obtener más información, consulte RFC
7231: codificación aceptada.
Algoritmos de compresión se están sujetas a un equilibrio entre la velocidad de compresión y la eficacia de la
compresión. Eficacia en este contexto hace referencia al tamaño de la salida después de la compresión. El tamaño
más pequeño se logra mediante la mayoría óptimo compresión.
Los encabezados implicados en la solicitud, enviar, almacenamiento en caché y recibir contenido comprimido se
describen en la tabla siguiente.

HEADER ROL

Accept-Encoding Envía desde el cliente al servidor para indicar el contenido


aceptable para el cliente de esquemas de codificación.

Content-Encoding Envía desde el servidor al cliente para indicar la codificación


del contenido de la carga.

Content-Length Cuando se produce la compresión, el Content-Length se


quita el encabezado, desde los cambios de contenido del
cuerpo cuando se comprime la respuesta.

Content-MD5 Cuando se produce la compresión, el Content-MD5 se quita


el encabezado, puesto que ha cambiado el contenido del
cuerpo y el hash ya no es válido.
HEADER ROL

Content-Type Especifica el tipo MIME del contenido. Debe especificar cada


respuesta su Content-Type . El middleware comprueba este
valor para determinar si se debe comprimir la respuesta. El
middleware especifica un conjunto de predeterminado tipos
MIME que puede codificar, pero puede reemplazar o agregar
un tipo MIME.

Vary Cuando envía el servidor con un valor de Accept-Encoding


a los clientes y servidores proxy, la Vary encabezado indica
al cliente o proxy que debe almacenar en caché (varían) las
respuestas en función del valor de la Accept-Encoding
encabezado de la solicitud. El resultado de la devolución del
contenido con el Vary: Accept-Encoding encabezado es
que ambos comprimen y sin comprimir las respuestas se
almacenan en caché por separado.

Puede explorar las características del Middleware de compresión de respuesta con el aplicación de ejemplo. El
ejemplo muestra:
La compresión de las respuestas de aplicación mediante gzip y proveedores de compresión personalizada.
Cómo agregar un tipo MIME a la lista predeterminada de tipos MIME para la compresión.

Package
Para incluir el software intermedio en el proyecto, agregue una referencia a la
Microsoft.AspNetCore.ResponseCompression paquete. Esta característica está disponible para aplicaciones que
tienen como destino ASP.NET Core 1.1 o posterior.
Para incluir el software intermedio en el proyecto, agregue una referencia a la
Microsoft.AspNetCore.ResponseCompression empaquetar o usar el Microsoft.AspNetCore.App metapackage
(ASP.NET Core 2.1 o posterior).

Configuración
El código siguiente muestra cómo habilitar el Middleware de compresión de respuesta con la compresión de gzip
predeterminada y para los tipos MIME predeterminado.

public class Startup


{
public void ConfigureServices(IServiceCollection services)
{
services.AddResponseCompression();
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)


{
app.UseResponseCompression();
}
}

NOTE
Utilice una herramienta como Fiddler, Firebug, o Postman para establecer el Accept-Encoding encabezado de solicitud y
estudiar los encabezados de respuesta, el tamaño y el cuerpo.
Enviar una solicitud a la aplicación de ejemplo sin la Accept-Encoding encabezado y observe que la respuesta es
sin comprimir. El Content-Encoding y Vary encabezados no están presentes en la respuesta.

Enviar una solicitud a la aplicación de ejemplo con el Accept-Encoding: gzip encabezado y observe que la
respuesta está comprimida. El Content-Encoding y Vary encabezados están presentes en la respuesta.

Proveedores
GzipCompressionProvider
Use la GzipCompressionProvider para comprimir las respuestas con gzip. Esto es el proveedor de compresión
predeterminado si no se especifica ninguno. Puede establecer la compresión de nivel con el
GzipCompressionProviderOptions.
El proveedor de compresión gzip usa de forma predeterminada el nivel de compresión más rápido
(CompressionLevel.Fastest), que no puede producir la compresión más eficaz. Si se desea la compresión más
eficaz, puede configurar el middleware de compresión óptima.

NIVEL DE COMPRESIÓN DESCRIPCIÓN

CompressionLevel.Fastest Compresión debe completar lo más rápido posible, incluso si


el resultado no está comprimido un rendimiento óptimo.

CompressionLevel.NoCompression Es necesario realizar ninguna compresión.

CompressionLevel.Optimal Las respuestas se deben comprimir un rendimiento óptimo,


incluso si la compresión tarda más tiempo en completarse.

ASP.NET Core 2.x


ASP.NET Core 1.x

public void ConfigureServices(IServiceCollection services)


{
services.AddResponseCompression(options =>
{
options.Providers.Add<GzipCompressionProvider>();
options.Providers.Add<CustomCompressionProvider>();
options.MimeTypes =
ResponseCompressionDefaults.MimeTypes.Concat(
new[] { "image/svg+xml" });
});

services.Configure<GzipCompressionProviderOptions>(options =>
{
options.Level = CompressionLevel.Fastest;
});
}

tipos MIME
El middleware especifica un conjunto predeterminado de tipos MIME para la compresión:
text/plain
text/css
application/javascript
text/html
application/xml
text/xml
application/json
text/json

Puede reemplazar o anexar los tipos MIME con las opciones de Middleware de compresión de respuesta. Tenga
en cuenta que MIME de comodines tipos, como text/* no son compatibles. La aplicación de ejemplo agrega un
tipo MIME para image/svg+xml y comprime y sirve de imagen de titular de ASP.NET Core (banner.svg).
ASP.NET Core 2.x
ASP.NET Core 1.x
public void ConfigureServices(IServiceCollection services)
{
services.AddResponseCompression(options =>
{
options.Providers.Add<GzipCompressionProvider>();
options.Providers.Add<CustomCompressionProvider>();
options.MimeTypes =
ResponseCompressionDefaults.MimeTypes.Concat(
new[] { "image/svg+xml" });
});

services.Configure<GzipCompressionProviderOptions>(options =>
{
options.Level = CompressionLevel.Fastest;
});
}

Proveedores personalizados
Puede crear implementaciones de compresión personalizada con ICompressionProvider. El EncodingName
representa la codificación que el control de contenido ICompressionProvider genera. El middleware que usa esta
información para elegir el proveedor basándose en la lista especificada en el Accept-Encoding encabezado de la
solicitud.
Mediante la aplicación de ejemplo, el cliente envía una solicitud con el Accept-Encoding: mycustomcompression
encabezado. El software intermedio utiliza la implementación de compresión personalizada y devuelve la
respuesta con un Content-Encoding: mycustomcompression encabezado. El cliente debe poder descomprimir la
codificación personalizada en orden para una implementación de la compresión personalizado para que funcione.
ASP.NET Core 2.x
ASP.NET Core 1.x

public void ConfigureServices(IServiceCollection services)


{
services.AddResponseCompression(options =>
{
options.Providers.Add<GzipCompressionProvider>();
options.Providers.Add<CustomCompressionProvider>();
options.MimeTypes =
ResponseCompressionDefaults.MimeTypes.Concat(
new[] { "image/svg+xml" });
});

services.Configure<GzipCompressionProviderOptions>(options =>
{
options.Level = CompressionLevel.Fastest;
});
}

public class CustomCompressionProvider : ICompressionProvider


{
public string EncodingName => "mycustomcompression";
public bool SupportsFlush => true;

public Stream CreateStream(Stream outputStream)


{
// Create a custom compression stream wrapper here
return outputStream;
}
}
Enviar una solicitud a la aplicación de ejemplo con el Accept-Encoding: mycustomcompression encabezado y
observe los encabezados de respuesta. El Vary y Content-Encoding encabezados están presentes en la respuesta.
El cuerpo de respuesta (no mostrado) no está comprimido en el ejemplo. No hay una implementación de la
compresión en el CustomCompressionProvider clase del ejemplo. Sin embargo, en el ejemplo se muestra donde se
implementa un algoritmo de compresión de este tipo.

Compresión con protocolo seguro


Las respuestas comprimidas a través de conexiones seguras pueden controlarse con el EnableForHttps opción,
que está deshabilitada de forma predeterminada. Utilice la compresión con páginas generadas dinámicamente
puede provocar problemas de seguridad como la CRIME y infracción ataques.

Agregar el encabezado Vary


Al comprimir las respuestas según el Accept-Encoding encabezado, hay potencialmente varias versiones
comprimidas de la respuesta y una versión sin comprimir. Para indicar a las memorias caché de cliente y servidor
proxy que existen varias versiones y deben almacenarse el Vary encabezado se agrega con un Accept-Encoding
valor. En el núcleo de ASP.NET 2.0 o posterior, el middleware agrega el Vary encabezado automáticamente
cuando se comprime la respuesta.
Al comprimir las respuestas según el Accept-Encoding encabezado, hay potencialmente varias versiones
comprimidas de la respuesta y una versión sin comprimir. Para indicar a las memorias caché de cliente y servidor
proxy que existen varias versiones y deben almacenarse el Vary encabezado se agrega con un Accept-Encoding
valor. En ASP.NET Core 1.x, agregar el Vary encabezado en la respuesta se realiza manualmente:
[!code-csharp]

Problema de middleware al estar detrás de un proxy inverso Nginx


Una vez procesada por Nginx, una solicitud de la Accept-Encoding se quita el encabezado. Esto evita que el
middleware de la compresión de la respuesta. Para obtener más información, consulte NGINX: compresión y
descompresión. Este problema se realiza un seguimiento por averiguar compresión de paso a través de Nginx
(BasicMiddleware n.º 123).

Trabajar con la compresión dinámica de IIS


Si tiene un módulo de compresión dinámica de IIS activo configurado en el nivel de servidor que desea
deshabilitar para una aplicación, puede hacerlo con un complemento de la web.config archivo. Para más
información, vea Disabling IIS modules (Deshabilitación de módulos de IIS ).

Solución de problemas
Utilice una herramienta como Fiddler, Firebug, o Postman, que permite establecer el Accept-Encoding
encabezado de solicitud y estudiar los encabezados de respuesta, el tamaño y el cuerpo. El Middleware de
compresión de respuesta comprime las respuestas que cumplen las condiciones siguientes:
El Accept-Encoding encabezado está presente con un valor de gzip , * , o codificación personalizada que
coincida con un proveedor de compresión personalizada que ha establecido. El valor no debe ser identity o
tener un valor de calidad (qvalue, q ) de 0 (cero).
El tipo MIME ( Content-Type ) debe establecerse y debe coincidir con un tipo MIME configurado en el
ResponseCompressionOptions.
La solicitud no debe incluir el Content-Range encabezado.
La solicitud debe utilizar inseguro protocolo (http), a menos que el protocolo seguro (https) se configura en las
opciones de Middleware de compresión de respuesta. Tenga en cuenta el peligro se ha descrito anteriormente
al habilitar la compresión de contenido segura.

Recursos adicionales
Inicio de aplicaciones
Middleware
Red de desarrollador de Mozilla: Codificación aceptada
RFC 7231 sección 3.1.2.1: Códigos contenidos
RFC 7230 sección 4.2.3: Codificación Gzip
Versión 4.3 de la especificación de formato del archivo GZIP
Migración a ASP.NET Core
10/05/2018 • 2 minutes to read • Edit Online

De ASP.NET a ASP.NET Core


Migración de ASP.NET a ASP.NET Core
Migración de ASP.NET MVC a ASP.NET Core MVC
Migración de ASP.NET Web API a ASP.NET Core Web API
Migración de la configuración
Migración de la autenticación y la identidad
Migración del uso de ClaimsPrincipal.Current
Migración de una pertenencia de ASP.NET a una identidad de ASP.NET Core
Migración de módulos HTTP a middleware

De ASP.NET Core 1.x a 2.0


Migración de ASP.NET Core 1.x a 2.0
Migración de la autenticación y la identidad
Migrar desde el núcleo de ASP.NET 2.0 a 2.1
22/06/2018 • 8 minutes to read • Edit Online

Por Rick Anderson


Vea Novedades de ASP.NET Core 2.1 para obtener información general de las nuevas características de ASP.NET
Core 2.1.
En este artículo:
Explica los conceptos básicos sobre la migración de una aplicación de ASP.NET Core 2.0 a 2.1.
Proporciona una visión general de los cambios realizados en las plantillas de aplicación web de ASP.NET Core.
Es una forma rápida de obtener una visión general de los cambios en 2.1:
Crear una aplicación web de ASP.NET Core 2.0 denominada WebApp1.
Confirmar la WebApp1 en un sistema de control de código fuente.
Elimine WebApp1 y cree una aplicación web de ASP.NET Core 2.1 denominada WebApp1 en el mismo lugar.
Revise los cambios en la versión 2.1.
Este artículo proporciona información general sobre la migración a ASP.NET Core 2.1. No contiene una lista
completa de todos los cambios necesarios para migrar a la versión 2.1. Algunos proyectos pueden requerir más
pasos dependiendo de las opciones seleccionadas cuando se creó el proyecto y las modificaciones realizadas en el
proyecto.

Actualice el archivo de proyecto para usar 2.1 versiones


Actualización de la .csproj archivo de proyecto:
Cambio <TargetFramework>netcoreapp2.0</TargetFramework> a la versión 2.1, que es
<TargetFramework>netcoreapp2.1</TargetFramework> .
Reemplazar la versión había especificada "Microsoft.AspNetCore.All" referencia de paquetes con la
referencia de paquete "Microsoft.AspNetCore.App" sin versión. Debe agregar las dependencias que se
quitaron de "Microsoft.AspNetCore.All". Vea migración desde Microsoft.AspNetCore.All a
Microsoft.AspNetCore.App y Microsoft.AspNetCore.App metapackage. Si tiene como destino .NET
Framework:
Agregar referencias de paquete individual en lugar de una referencia de paquete meta.
Actualizar cada referencia de paquete a 2.1.
Quite todas las referencias a <DotNetCliToolReference> elementos para los paquetes
"Microsoft.AspNetCore", "Microsoft.VisualStudio" y "Microsoft.EntityFrameworkCore". Estas herramientas
se han reemplazado por herramientas globales.
El marcado siguiente se muestra la plantilla genera 2.0 .csproj archivo de proyecto:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
<UserSecretsId>aspnet-{Project Name}-{GUID}</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.0.3" PrivateAssets="All" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.0.4"
PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.3" />
<DotNetCliToolReference Include="Microsoft.Extensions.SecretManager.Tools" Version="2.0.2" />
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.4" />
</ItemGroup>
</Project>

El marcado siguiente se muestra la plantilla genera 2.1 .csproj archivo de proyecto:

<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<UserSecretsId>aspnet-{Project Name}-{GUID}</UserSecretsId>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.1.0"
PrivateAssets="All" />
</ItemGroup>

</Project>

Reglas para proyectos tienen como destino el tiempo de ejecución


compartido
ASP.NET Core incluye los tiempos de ejecución compartido siguientes:
Microsoft.AspNetCore.App
Microsoft.AspNetCore.All
Reglas para los proyectos de destino es el runtime compartido:
Los proyectos que hacen referencia a Microsoft.AspNetCore.All o Microsoft.AspNetCore.App debe hacer
referencia a paquetes, Microsoft.Net.Sdk.Web Sdk.
Los proyectos que hacen referencia a paquetes o proyectos que hacen referencia a transitivamente
Microsoft.AspNetCore.All o Microsoft.AspNetCore.App :
Debe hacer referencia a la Microsoft.Net.Sdk.Web Sdk y,
Debe hacer referencia el mismo paquete en tiempo de ejecución compartido. Por ejemplo, si hace
referencia a LibraryA Microsoft.AspNetCore.App , debe hacer referencia a ningún proyecto hace referencia
a él Microsoft.AspNetCore.App .
Proyectos ejecutables:
Son proyectos que contienen aplicaciones que se inician con dotnet run .
Son aplicaciones que se ejecutan y proyectos para las aplicaciones de prueba.
Proyectos ejecutables no deben especificar la versión. El Sdk especifica la versión implícitamente a través
<PackageReference Include="Microsoft.AspNetCore.App" /> .
Los proyectos que se hace referencia, es decir:
Los proyectos que no son el punto de entrada, y
Las referencias del proyecto Microsoft.AspNetCore.All o Microsoft.AspNetCore.App .
Los proyectos que se hace referencia deben especificar una versión del paquete.

Cambios para aprovechar las ventajas de las expresiones nueva basada


en código que se recomiendan en ASP.NET Core 2.1
Cambios a la página principal
Las siguientes imágenes muestran los cambios realizados en la plantilla genera Program.cs archivo.

La imagen anterior muestra la versión 2.0 con las eliminaciones en color rojo.
La siguiente imagen muestra el código 2.1. El código en verde sustituye a la versión 2.0:

El código siguiente muestra la versión 2.1 de Program.cs:

namespace WebApp1
{
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>


WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
}
}
El nuevo Main reemplaza la llamada a BuildWebHost con CreateWebHostBuilder. IWebHostBuilder se agregó para
admitir un nuevo infraestructura de pruebas de integración.
Cambios en el inicio
El código siguiente muestra los cambios al código de plantilla 2.1 generado. Todos los cambios se agregan recién
código, salvo que UseBrowserLink se ha quitado:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace WebApp1
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}

public IConfiguration Configuration { get; }

public void ConfigureServices(IServiceCollection services)


{
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given
request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});

services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)


{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();

app.UseMvc();
}
}
}

Los cambios de código anterior se detallan en:


Compatibilidad con GDPR en ASP.NET Core para CookiePolicyOptions y UseCookiePolicy .
Protocolo de seguridad de transporte estrictos de HTTP (HSTS ) para UseHsts .
Requerir HTTPS para UseHttpsRedirection .
SetCompatibilityVersion para SetCompatibilityVersion(CompatibilityVersion.Version_2_1) .
Cambios en el código de autenticación
ASP.NET Core 2.1 proporciona ASP.NET Core Identity como un biblioteca de clases de Razor. Si no ha realizado
importantes cambios en la plantilla 2.0 generan código de identidad, tenga en cuenta el enfoque de actualización
siguiente:
Elimine el código existente de identidad.
Aplicar la técnica scaffolding identidad en el proyecto.

Cambios en las páginas de Razor los proyectos de archivos de Razor


El archivo de diseño
Pages/_Layout.cshtml mueve a Pages/Shared/_Layout.cshtml
El Layout.cshtml archivo tiene los siguientes cambios:
<partial name="_CookieConsentPartial" /> se agrega. Para obtener más información, consulte GDPR
admite en ASP.NET Core.
cambios de jQuery de 2.2.0 a 3.3.1
_ValidationScriptsPartial.cshtml
Pages/_ValidationScriptsPartial.cshtml mueve a Pages/Shared/_ValidationScriptsPartial.cshtml
jQuery.Validate/1.14.0 cambia a jquery.validate/1.17.0
Nuevos archivos
Se agregan los siguientes archivos:
Privacy.cshtml
Privacy.cshtml.cs
Vea GDPR admite en ASP.NET Core para obtener información sobre los archivos anteriores.

Cambios en los archivos de Razor de proyectos MVC


El archivo de diseño
El Layout.cshtml archivo tiene los siguientes cambios:
<partial name="_CookieConsentPartial" /> se agrega.
cambios de jQuery de 2.2.0 a 3.3.1
_ValidationScriptsPartial.cshtml
jQuery.Validate/1.14.0 cambia a jquery.validate/1.17.0
Nuevos archivos y los métodos de acción
Se ha agregado lo siguiente:
Views/Home/Privacy.cshtml
El Privacy se agrega el método de acción para el controlador Home.
Vea GDPR admite en ASP.NET Core para obtener información sobre los archivos anteriores.

Cambios adicionales
SetCompatibilityVersion
Configuración de transporte
Migración de ASP.NET a ASP.NET Core
21/06/2018 • 13 minutes to read • Edit Online

Por Isaac Levin


Este artículo sirve de guía de referencia para migrar aplicaciones de ASP.NET a ASP.NET Core.

Requisitos previos
.NET Core SDK 2.0 or later

Versiones de .NET Framework de destino


Los proyectos de ASP.NET Core proporcionan a los desarrolladores la flexibilidad de usar la versión .NET Core,
.NET Framework o ambas. Vea Selección entre .NET Core y .NET Framework para aplicaciones de servidor para
determinar qué plataforma de destino es más adecuada.
Cuando se usa la versión .NET Framework, es necesario que los proyectos hagan referencia a paquetes de NuGet
individuales.
Cuando se usa .NET Core, se pueden eliminar numerosas referencias explícitas del paquete gracias al metapaquete
de ASP.NET Core. Instale el metapaquete Microsoft.AspNetCore.All en el proyecto:

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" />
</ItemGroup>

Cuando se usa el metapaquete, con la aplicación no se implementa ningún paquete al que se hace referencia en el
metapaquete. El almacén de tiempo de ejecución de .NET Core incluye estos activos que están compilados
previamente para mejorar el rendimiento. Vea Microsoft.AspNetCore.All metapackage for ASP.NET Core 2.x
(Metapaquete Microsoft.AspNetCore.All para ASP.NET Core 2.x) para obtener más detalles.

Diferencias en la estructura de proyecto


El formato de archivo .csproj se ha simplificado en ASP.NET Core. Estos son algunos cambios importantes:
La inclusión explícita de archivos no es necesaria para que se consideren parte del proyecto. Esto reduce el
riesgo de conflictos al fusionar XML cuando se trabaja en equipos grandes.
No hay referencias de GUID a otros proyectos, lo cual mejora la legibilidad del archivo.
El archivo se puede editar sin descargarlo en Visual Studio:
Reemplazo del archivo Global.asax
ASP.NET Core introdujo un nuevo mecanismo para arrancar una aplicación. El punto de entrada para las
aplicaciones ASP.NET es el archivo Global.asax. En el archivo Global.asax se controlan tareas como la
configuración de enrutamiento y los registros de filtro y de área.

public class MvcApplication : System.Web.HttpApplication


{
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
}
}

Este enfoque acopla la aplicación y el servidor en el que está implementada de forma que interfiere con la
implementación. En un esfuerzo por desacoplar, se introdujo OWIN para ofrecer una forma más limpia de usar
varios marcos de trabajo de manera conjunta. OWIN proporciona una canalización para agregar solo los módulos
necesarios. El entorno de hospedaje toma una función de inicio para configurar servicios y la canalización de
solicitud de la aplicación. Startup registra un conjunto de middleware en la aplicación. Para cada solicitud, la
aplicación llama a cada uno de los componentes de middleware con el puntero principal de una lista vinculada a un
conjunto de controladores existente. Cada componente de middleware puede agregar uno o varios controladores a
la canalización de control de la solicitud. Esto se consigue mediante la devolución de una referencia al controlador
que ahora es el primero de la lista. Cada controlador se encarga de recordar e invocar el controlador siguiente en
la lista. Con ASP.NET Core, el punto de entrada a una aplicación es Startup y ya no se tiene dependencia de
Global.asax. Cuando utilice OWIN con .NET Framework, use algo parecido a lo siguiente como canalización:
using Owin;
using System.Web.Http;

namespace WebApi
{
// Note: By default all requests go through this OWIN pipeline. Alternatively you can turn this off by
adding an appSetting owin:AutomaticAppStartup with value “false”.
// With this turned off you can still have OWIN apps listening on specific routes by adding routes in
global.asax file using MapOwinPath or MapOwinRoute extensions on RouteTable.Routes
public class Startup
{
// Invoked once at startup to configure your application.
public void Configuration(IAppBuilder builder)
{
HttpConfiguration config = new HttpConfiguration();
config.Routes.MapHttpRoute("Default", "{controller}/{customerID}", new { controller = "Customer",
customerID = RouteParameter.Optional });

config.Formatters.XmlFormatter.UseXmlSerializer = true;
config.Formatters.Remove(config.Formatters.JsonFormatter);
// config.Formatters.JsonFormatter.UseDataContractJsonSerializer = true;

builder.UseWebApi(config);
}
}
}

Esto configura las rutas predeterminadas y tiene como valor predeterminado XmlSerialization a través de Json.
Agregue otro middleware a esta canalización según sea necesario (carga de servicios, opciones de configuración,
archivos estáticos, etcétera).
ASP.NET Core usa un enfoque similar, pero no depende de OWIN para controlar la entrada. En lugar de eso, usa el
método Program.cs Main (similar a las aplicaciones de consola) y Startup se carga a través de ahí.

using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;

namespace WebApplication2
{
public class Program
{
public static void Main(string[] args)
{
BuildWebHost(args).Run();
}

public static IWebHost BuildWebHost(string[] args) =>


WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.Build();
}
}

Startup debe incluir un método Configure . En Configure , agregue el middleware necesario a la canalización. En
el ejemplo siguiente (de la plantilla de sitio web predeterminada), se usan varios métodos de extensión para
configurar la canalización con compatibilidad para:
BrowserLink
Páginas de error
Archivos estáticos
ASP.NET Core MVC
identidad

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)


{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();

if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}

app.UseStaticFiles();

app.UseIdentity();

app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}

El host y la aplicación se han desacoplado, lo que proporciona la flexibilidad de pasar a una plataforma diferente en
el futuro.

NOTE
Para acceder a referencias más detalladas sobre el inicio de ASP.NET Core y middleware, consulte Inicio de la aplicación en
ASP.NET Core.

Almacenamiento de valores de configuración


ASP.NET admite el almacenamiento de valores de configuración. Estos valores de configuración se usan, por
ejemplo, para admitir el entorno donde se implementan las aplicaciones. Antes se solían almacenar todos los pares
de clave y valor personalizados en la sección <appSettings> del archivo Web.config:

<appSettings>
<add key="UserName" value="User" />
<add key="Password" value="Password" />
</appSettings>

Las aplicaciones leen esta configuración mediante la colección ConfigurationManager.AppSettings en el espacio de


nombres System.Configuration :

string userName = System.Web.Configuration.ConfigurationManager.AppSettings["UserName"];


string password = System.Web.Configuration.ConfigurationManager.AppSettings["Password"];

ASP.NET Core puede almacenar datos de configuración para la aplicación en cualquier archivo y cargarlos como
parte del arranque de middleware. El archivo predeterminado que se usa en las plantillas de proyecto es
appSettings.JSON:
{
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
},
// Here is where you can supply custom configuration settings, Since it is is JSON, everything is
represented as key: value pairs
// Name of section is your choice
"AppConfiguration": {
"UserName": "UserName",
"Password": "Password"
}
}

La carga de este archivo en una instancia de IConfiguration dentro de la aplicación se realiza en Startup.cs:

public Startup(IConfiguration configuration)


{
Configuration = configuration;
}

public IConfiguration Configuration { get; }

La aplicación lee de Configuration para obtener la configuración:

string userName = Configuration.GetSection("AppConfiguration")["UserName"];


string password = Configuration.GetSection("AppConfiguration")["Password"];

Existen extensiones de este enfoque para lograr que el proceso sea más sólido, como el uso de inserción de
dependencias para cargar un servicio con estos valores. El enfoque de la inserción de dependencias proporciona
un conjunto fuertemente tipado de objetos de configuración.

// Assume AppConfiguration is a class representing a strongly-typed version of AppConfiguration section


services.Configure<AppConfiguration>(Configuration.GetSection("AppConfiguration"));

NOTE
Para acceder a referencias más detalladas sobre la configuración de ASP.NET Core, consulte Configuración en ASP.NET Core.

Inserción de dependencias nativa


Un objetivo importante al compilar aplicaciones grandes y escalables es lograr el acoplamiento flexible de los
componentes y los servicios. La inserción de dependencias es una técnica popular para lograrlo y se trata de un
componente nativo de ASP.NET Core.
En las aplicaciones ASP.NET, los desarrolladores se sirven de una biblioteca de terceros para implementar la
inserción de dependencias. Una de estas bibliotecas es Unity, suministrada por Microsoft Patterns & Practices.
Un ejemplo de configuración de la inserción de dependencias con Unity consiste en implementar
IDependencyResolver que encapsula un UnityContainer :
using Microsoft.Practices.Unity;
using System;
using System.Collections.Generic;
using System.Web.Http.Dependencies;

public class UnityResolver : IDependencyResolver


{
protected IUnityContainer container;

public UnityResolver(IUnityContainer container)


{
if (container == null)
{
throw new ArgumentNullException("container");
}
this.container = container;
}

public object GetService(Type serviceType)


{
try
{
return container.Resolve(serviceType);
}
catch (ResolutionFailedException)
{
return null;
}
}

public IEnumerable<object> GetServices(Type serviceType)


{
try
{
return container.ResolveAll(serviceType);
}
catch (ResolutionFailedException)
{
return new List<object>();
}
}

public IDependencyScope BeginScope()


{
var child = container.CreateChildContainer();
return new UnityResolver(child);
}

public void Dispose()


{
Dispose(true);
}

protected virtual void Dispose(bool disposing)


{
container.Dispose();
}
}

Cree una instancia de UnityContainer , registre el servicio y establezca la resolución de dependencias de


HttpConfiguration en la nueva instancia de UnityResolver para el contenedor:
public static void Register(HttpConfiguration config)
{
var container = new UnityContainer();
container.RegisterType<IProductRepository, ProductRepository>(new HierarchicalLifetimeManager());
config.DependencyResolver = new UnityResolver(container);

// Other Web API configuration not shown.


}

Inserte IProductRepository cuando sea necesario:

public class ProductsController : ApiController


{
private IProductRepository _repository;

public ProductsController(IProductRepository repository)


{
_repository = repository;
}

// Other controller methods not shown.


}

Dado que la inserción de dependencia forma parte de ASP.NET Core, puede agregar el servicio en el método
ConfigureServices de Startup.cs:

public void ConfigureServices(IServiceCollection services)


{
// Add application services.
services.AddTransient<IProductRepository, ProductRepository>();
}

El repositorio se puede insertar en cualquier lugar, como ocurría con Unity.

NOTE
Para acceder a referencias más detalladas sobre la inserción de dependencias en ASP.NET Core, consulte Inserción de
dependencias en ASP.NET Core.

Proporcionar archivos estáticos


Una parte importante del desarrollo web es la capacidad de trabajar con activos estáticos de cliente. Los ejemplos
más comunes de archivos estáticos son HTML, CSS, JavaScript e imágenes. Estos archivos deben guardarse en la
ubicación de publicación de la aplicación (o la red de entrega de contenido) y es necesario hacer referencia a ellos
para que una solicitud los pueda cargar. Este proceso ha cambiado en ASP.NET Core.
En ASP.NET, los archivos estáticos se almacenan en directorios distintos y se hace referencia a ellos en las vistas.
En ASP.NET Core, los archivos estáticos se almacenan en la "raíz web" (<raíz del contenido>/wwwroot), a menos
que se configuren de otra manera. Los archivos se cargan en la canalización de solicitud mediante la invocación del
método de extensión UseStaticFiles desde Startup.Configure :

public void Configure(IApplicationBuilder app)


{
app.UseStaticFiles();
}
NOTE
Si el destino es .NET Framework, debe instalarse el paquete de NuGet Microsoft.AspNetCore.StaticFiles .

Por ejemplo, el explorador puede acceder a un recurso de imagen en la carpeta wwwroot/images en una ubicación
como http://<app>/images/<imageFileName> .

NOTE
Para acceder a referencias más detalladas sobre cómo trabajar con archivos estáticos en ASP.NET Core, consulte Archivos
estáticos.

Recursos adicionales
Traslado a .NET Core: bibliotecas
Migrar de MVC de ASP.NET a ASP.NET Core MVC
22/06/2018 • 15 minutes to read • Edit Online

Por Rick Anderson, Daniel Roth, Steve Smith, y Scott Addie


Este artículo muestra cómo empezar a migrar un proyecto de MVC de ASP.NET a MVC de ASP.NET Core. En el
proceso, resalta muchas de las cosas que han cambiado respecto a ASP.NET MVC. La migración de ASP.NET
MVC es un proceso en varias fases y este artículo trata la instalación inicial, controladores básicos y vistas,
contenido estático y las dependencias del lado cliente. Migrar la configuración y el código de identidad que se
encuentra en muchos proyectos de ASP.NET MVC tratan otros artículos.

NOTE
Los números de versión en los ejemplos podrían no estar actualizados. Debe actualizar los proyectos en consecuencia.

Crear el proyecto de MVC de ASP.NET de inicio


Para demostrar la actualización, comenzaremos mediante la creación de una aplicación de ASP.NET MVC. Crear
con el nombre WebApp1 para el espacio de nombres coincida con el proyecto de ASP.NET Core que creamos en
el paso siguiente.
Opcional: cambiar el nombre de la solución de WebApp1 a Mvc5. Visual Studio muestra el nuevo nombre de la
solución (Mvc5), lo que facilita indicar este proyecto desde el proyecto siguiente.

Crear el proyecto de ASP.NET Core


Crear un nuevo vacía aplicación web de ASP.NET Core con el mismo nombre que el proyecto anterior (WebApp1)
para que coincidan con los espacios de nombres en los dos proyectos. Tener el mismo espacio de nombres resulta
más fácil copiar el código entre los dos proyectos. Tendrá que crear este proyecto en un directorio diferente que el
proyecto anterior para utilizar el mismo nombre.
Opcional: crear una nueva aplicación ASP.NET Core usando la aplicación Web plantilla de proyecto. Denomine
el proyecto WebApp1y seleccione una opción de autenticación de cuentas de usuario individuales. Cambiar
el nombre de esta aplicación FullAspNetCore. Creación de este proyecto le permite ahorrar tiempo en la
conversión. También puede buscar en el código generado de plantilla para ver el resultado final o copiar el
código al proyecto de conversión. También es útil cuando bloquea en un paso de la conversión que se compara
con el proyecto de plantilla generado.

Configurar el sitio para usar MVC


Cuando el destino es .NET Core, el metapackage de ASP.NET Core se agrega al proyecto, denominado
Microsoft.AspNetCore.All de forma predeterminada. Este paquete contiene paquetes como
Microsoft.AspNetCore.Mvc y Microsoft.AspNetCore.StaticFiles . Si el destino es .NET Framework, las
referencias del paquete tiene que mostrar individualmente en el archivo *.csproj.
Microsoft.AspNetCore.Mvc es el marco de MVC de ASP.NET Core. Microsoft.AspNetCore.StaticFiles es el
controlador de archivos estáticos. El tiempo de ejecución de ASP.NET Core es modular y explícitamente debe
participar en servir archivos estáticos (vea archivos estáticos).
Abra la Startup.cs y cambie el código para que coincida con lo siguiente:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;

namespace WebApp1
{
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit
https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
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)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}

app.UseStaticFiles();

app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}

El UseStaticFiles método de extensión agrega el controlador de archivos estáticos. Como se mencionó


anteriormente, el tiempo de ejecución ASP.NET es modular y explícitamente debe participar en servir archivos
estáticos. El UseMvc método de extensión agrega enrutamiento. Para obtener más información, consulte inicio de
la aplicación y enrutamiento.

Agregar un controlador y una vista


En esta sección, agregará un controlador mínima y la vista para que actúe como marcadores de posición para el
controlador de MVC de ASP.NET y las vistas a migrar en la sección siguiente.
Agregar un controladores carpeta.
Agregar un clase de controlador denominado HomeController.cs a la controladores carpeta.
Agregar un vistas carpeta.
Agregar un vistas/inicio carpeta.
Agregar un vista Razor denominado Index.cshtml a la vistas/inicio carpeta.

A continuación se muestra la estructura del proyecto:


Reemplace el contenido de la Views/Home/Index.cshtml archivo con lo siguiente:

<h1>Hello world!</h1>

Ejecute la aplicación.

Vea controladores y vistas para obtener más información.


Ahora que tenemos un proyecto ASP.NET Core trabajo mínimo, se podemos empezar a migrar la funcionalidad
desde el proyecto de MVC de ASP.NET. Es preciso mover lo siguiente:
contenido del lado cliente (CSS, fuentes y secuencias de comandos)
controladores
vistas
modelos
Cómo agrupar
filtros
Registro de entrada/salida, la identidad (Esto se hace en el siguiente tutorial.)

Controladores y vistas
Copiar cada uno de los métodos de ASP.NET MVC HomeController al nuevo HomeController . Tenga en
cuenta que en ASP.NET MVC, tipo de valor devuelto de método de acción de controlador de la plantilla
integrada ActionResult; en el núcleo de ASP.NET MVC, los métodos de acción devueltos IActionResult en
su lugar. ActionResult implementa IActionResult , por lo que no es necesario para cambiar el tipo de valor
devuelto de los métodos de acción.
Copia la About.cshtml, Contact.cshtml, y Index.cshtml archivos de vista Razor desde el proyecto de MVC de
ASP.NET para el proyecto de ASP.NET Core.
Ejecutar la aplicación de ASP.NET Core y cada método de prueba. No hemos migramos los estilos o el
archivo de diseño, por lo que las vistas representadas solo contienen el contenido de los archivos de vista.
No tendrá los vínculos de archivo generado de diseño para la About y Contact vistas, por lo que tendrá
que invocar a los mismos desde el explorador (reemplace 4492 con el número de puerto utilizado en el
proyecto).
http://localhost:4492/home/about

http://localhost:4492/home/contact

Tenga en cuenta la falta de un estilo y elementos de menú. Esto lo corregiremos en la sección siguiente.

Contenido estático
En versiones anteriores de ASP.NET MVC, contenido estático se alojó desde la raíz del proyecto web y se ha
combinado con archivos de servidor. En el núcleo de ASP.NET, contenido estático se hospeda en el wwwroot
carpeta. Desea copiar el contenido estático de la aplicación de MVC de ASP.NET anterior a la wwwroot carpeta en
el proyecto de ASP.NET Core. En esta conversión de ejemplo:
Copia la favicon.ico archivo desde el proyecto MVC anterior a la wwwroot carpeta del proyecto de ASP.NET
Core.
La antigua ASP.NET MVC project utiliza Bootstrap para su aplicación de estilos y almacena el arranque de los
archivos del contenido y Scripts carpetas. Hace referencia a la plantilla, que genera el proyecto de MVC de
ASP.NET anterior, arranque en el archivo de diseño (Views/Shared/_Layout.cshtml). Podría copiar el bootstrap.js y
bootstrap.css proyecto de archivos de ASP.NET MVC a la wwwroot carpeta en el nuevo proyecto. En su lugar,
vamos a agregar compatibilidad para el arranque (y otras bibliotecas de cliente) con CDN en la sección siguiente.

Migrar el archivo de diseño


Copia la _ViewStart.cshtml archivo desde el proyecto de MVC de ASP.NET anterior vistas carpeta en el
proyecto de ASP.NET Core vistas carpeta. El _ViewStart.cshtml archivo no ha cambiado en MVC de
ASP.NET Core.
Crear un vistas/compartidas carpeta.
Opcional: copia _ViewImports.cshtml desde el FullAspNetCore del proyecto MVC vistas carpeta en el
proyecto de ASP.NET Core Vistas carpeta. Quite las declaraciones de espacio de nombres en el
_ViewImports.cshtml archivo. El _ViewImports.cshtml archivo proporciona los espacios de nombres para
todos los archivos de vista y la pone aplicaciones auxiliares de etiquetas. Aplicaciones auxiliares de
etiquetas se usan en el nuevo archivo de diseño. El _ViewImports.cshtml archivo es una novedad de
ASP.NET Core.
Copia la _Layout.cshtml archivo desde el proyecto de MVC de ASP.NET anterior vistas/compartidas carpeta
en el proyecto de ASP.NET Core vistas/compartidas carpeta.
Abra _Layout.cshtml de archivos y realice los cambios siguientes (el código completo se muestra a continuación):
Reemplace @Styles.Render("~/Content/css") con un <link> elemento que se va a cargar bootstrap.css (ver
abajo).
Quite @Scripts.Render("~/bundles/modernizr") .
Convierta en comentario la @Html.Partial("_LoginPartial") línea (incluya la línea con @*...*@ ). Se tendrá
que volver a él en un tutorial posterior.
Reemplace @Scripts.Render("~/bundles/jquery") con un <script> elemento (ver abajo).
Reemplace @Scripts.Render("~/bundles/bootstrap") con un <script> elemento (ver abajo).

El marcado de reemplazo para la inclusión de CSS de arranque:

<link rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
crossorigin="anonymous">

El marcado de reemplazo de jQuery y la inclusión de JavaScript de arranque:

<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"
integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa"
crossorigin="anonymous"></script>

La actualización _Layout.cshtml archivo se muestra a continuación:


<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>@ViewBag.Title - My ASP.NET Application</title>
<link rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
crossorigin="anonymous">
</head>
<body>
<div class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-
collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
@Html.ActionLink("Application name", "Index", "Home", new { area = "" }, new { @class =
"navbar-brand" })
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li>@Html.ActionLink("Home", "Index", "Home")</li>
<li>@Html.ActionLink("About", "About", "Home")</li>
<li>@Html.ActionLink("Contact", "Contact", "Home")</li>
</ul>
@*@Html.Partial("_LoginPartial")*@
</div>
</div>
</div>
<div class="container body-content">
@RenderBody()
<hr />
<footer>
<p>&copy; @DateTime.Now.Year - My ASP.NET Application</p>
</footer>
</div>

<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"
integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa"
crossorigin="anonymous"></script>
@RenderSection("scripts", required: false)
</body>
</html>

Ver el sitio en el explorador. Ahora deberían cargarse correctamente, con los estilos esperados en su lugar.
Opcional: desea intentar usar el nuevo archivo de diseño. Para este proyecto se puede copiar el archivo de
diseño de la FullAspNetCore proyecto. El nuevo archivo de diseño usa aplicaciones auxiliares de etiquetas y
tiene otras mejoras.

Configurar la agrupación y minificación


Para obtener información sobre cómo configurar la agrupación y minificación, consulte unión y Minificación.

Solucionar errores de HTTP 500


Hay muchos problemas que pueden provocar un mensaje de error HTTP 500 que no contienen ninguna
información en el origen del problema. Por ejemplo, si la Views/_ViewImports.cshtml archivo contiene un espacio
de nombres que no existe en el proyecto, obtendrá un error HTTP 500. De forma predeterminada en las
aplicaciones ASP.NET Core, el UseDeveloperExceptionPage extensión se agrega a la IApplicationBuilder y se
ejecuta cuando la configuración es desarrollo. Esto se detalla en el código siguiente:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;

namespace WebApp1
{
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?
LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
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)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}

app.UseStaticFiles();

app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}

ASP.NET Core convierte las excepciones no controladas en una aplicación web en las respuestas de error HTTP
500. Normalmente, los detalles del error no se incluyen en estas respuestas para evitar la divulgación de
información potencialmente confidencial acerca del servidor. Vea mediante la página de excepción para
desarrolladores en controlar los errores para obtener más información.

Recursos adicionales
Desarrollo del lado cliente
Aplicaciones auxiliares de etiquetas
Migrar de API Web de ASP.NET a ASP.NET Core
23/06/2018 • 14 minutes to read • Edit Online

Por Steve Smith y Scott Addie


Las API Web son servicios HTTP que llegan a una amplia gama de clientes, incluidos los exploradores y
dispositivos móviles. Núcleo de ASP.NET MVC incluye compatibilidad para la creación de las API Web
proporcionan una manera única y coherente de la creación de aplicaciones web. En este artículo, se muestran los
pasos necesarios para migrar una implementación de la API Web de ASP.NET Web API para MVC de ASP.NET
Core.
Vea o descargue el código de ejemplo (cómo descargarlo)

Proyecto de revisión ASP.NET Web API


Este artículo utiliza el proyecto de ejemplo, ProductsApp, creado en el artículo Introducción a ASP.NET Web API 2
como punto de partida. En el proyecto, un proyecto de ASP.NET Web API simple se configura como se indica a
continuación.
En Global.asax.cs, se realiza una llamada a WebApiConfig.Register :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Http;
using System.Web.Routing;

namespace ProductsApp
{
public class WebApiApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
GlobalConfiguration.Configure(WebApiConfig.Register);
}
}
}

WebApiConfig se define en App_Start, y tiene solo un estático Register método:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Http;

namespace ProductsApp
{
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Web API configuration and services

// Web API routes


config.MapHttpAttributeRoutes();

config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
}
}

Esta clase configura atributo enrutamiento, aunque realmente no se usa en el proyecto. También configura la tabla
de enrutamiento, que se usa por la API Web de ASP.NET. En este caso, ASP.NET Web API esperará direcciones
URL para que coincida con el formato /api/ {controller } / {id }, con {id } que es opcional.
El ProductsApp proyecto incluye un solo controlador simple, que hereda de ApiController y expone dos métodos:
using ProductsApp.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Web.Http;

namespace ProductsApp.Controllers
{
public class ProductsController : ApiController
{
Product[] products = new Product[]
{
new Product { Id = 1, Name = "Tomato Soup", Category = "Groceries", Price = 1 },
new Product { Id = 2, Name = "Yo-yo", Category = "Toys", Price = 3.75M },
new Product { Id = 3, Name = "Hammer", Category = "Hardware", Price = 16.99M }
};

public IEnumerable<Product> GetAllProducts()


{
return products;
}

public IHttpActionResult GetProduct(int id)


{
var product = products.FirstOrDefault((p) => p.Id == id);
if (product == null)
{
return NotFound();
}
return Ok(product);
}
}
}

Por último, el modelo, producto, utilizada por el ProductsApp, es una clase simple:

namespace ProductsApp.Models
{
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public string Category { get; set; }
public decimal Price { get; set; }
}
}

Ahora que tenemos un proyecto simple desde el que se va a iniciar, podemos demostrar cómo migrar este
proyecto de API Web MVC de ASP.NET Core.

Crear el proyecto de destino


Con Visual Studio, cree una solución nueva y vacía y asígnele el nombre WebAPIMigration. Agregar existente
ProductsApp proyecto al, a continuación, agregue un nuevo proyecto de aplicación de ASP.NET Core Web a la
solución. Asigne al nuevo proyecto ProductsCore.
A continuación, elija la plantilla de proyecto de API Web. Se migrará el ProductsApp contenido para este nuevo
proyecto.

Eliminar el Project_Readme.html archivo desde el nuevo proyecto. La solución debe tener el siguiente aspecto:
Migrar la configuración
Ya no usa ASP.NET Core Global.asax, web.config, o App_Start carpetas. En su lugar, se realizan todas las tareas de
inicio en Startup.cs en la raíz del proyecto (vea inicio de la aplicación). En ASP.NET MVC de núcleo, el
enrutamiento basado en atributos ahora se incluye de forma predeterminada cuando UseMvc() se denomina; y, es
el enfoque recomendado para configurar las rutas de Web API (y es la forma en que el proyecto de inicio de la
API Web controla el enrutamiento).
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace ProductsCore
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}

public IConfiguration Configuration { get; }

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
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, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();

app.UseMvc();
}
}
}

Suponiendo que desea utilizar el enrutamiento de atributo en el proyecto en el futuro, se necesita ninguna
configuración adicional. Basta con aplicar los atributos según sea necesario para los controladores y acciones,
como se hace en el ejemplo ValuesController clase que se incluye en el proyecto de inicio de la API Web:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;

namespace ProductsCore.Controllers
{
[Route("api/[controller]")]
public class ValuesController : Controller
{
// GET api/values
[HttpGet]
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}

// GET api/values/5
[HttpGet("{id}")]
public string Get(int id)
{
return "value";
}

// POST api/values
[HttpPost]
public void Post([FromBody]string value)
{
}

// PUT api/values/5
[HttpPut("{id}")]
public void Put(int id, [FromBody]string value)
{
}

// DELETE api/values/5
[HttpDelete("{id}")]
public void Delete(int id)
{
}
}
}

Tenga en cuenta la presencia de [controlador ] en la línea 8. Enrutamiento basado en atributos ahora es compatible
con determinados símbolos (tokens), como [controlador ] y [acción]. Estos tokens se reemplazan en tiempo de
ejecución con el nombre del controlador o acción, respectivamente, al que se ha aplicado el atributo. Esto sirve
para reducir el número de cadenas mágicos en el proyecto y garantiza que las rutas se mantendrá sincronizadas
con sus controladores correspondientes y las acciones cuando se aplican refactorizaciones rename automática.
Para migrar el controlador de API de productos, debemos primero copiaremos ProductsController al nuevo
proyecto. A continuación, basta con incluir el atributo de ruta en el controlador:

[Route("api/[controller]")]

También debe agregar el [HttpGet] atribuir a los dos métodos, ya que se deberían llamar a través de HTTP Get.
Incluir la expectativa de un parámetro "id" en el atributo para GetProduct() :
// /api/products
[HttpGet]
...

// /api/products/1
[HttpGet("{id}")]

En este momento, el enrutamiento está configurado correctamente; Sin embargo, aún no podemos probarlo. Se
deben realizar cambios adicionales antes de ProductsController se compilará.

Migrar los modelos y controladores


El último paso del proceso de migración para este proyecto de API Web simple es copiar a través de los
controladores y los modelos que se usan. En este caso, basta con copiar Controllers/ProductsController.cs desde el
proyecto original al nuevo. A continuación, copie toda la carpeta modelos desde el proyecto original al nuevo.
Ajustar los espacios de nombres para que coincida con el nuevo nombre de proyecto (ProductsCore). En este
momento, puede compilar la aplicación y encontrará un número de errores de compilación. Por lo general, estos
deben estar en las siguientes categorías:
ApiController no existe
System.Web.Http no existe el espacio de nombres
IHttpActionResult no existe
Afortunadamente, estos son muy fáciles corregir:
Cambio ApiController a controlador (puede que necesite agregar con Microsoft.AspNetCore.Mvc)
Elimine cualquiera con la instrucción que hace referencia a System.Web.Http
Cambiar cualquier método devolver IHttpActionResult para devolver un IActionResult
Una vez que estos cambios se han realizado y que no use instrucciones using quita, el migrados
ProductsController clase tiene el siguiente aspecto:
using Microsoft.AspNetCore.Mvc;
using ProductsCore.Models;
using System.Collections.Generic;
using System.Linq;

namespace ProductsCore.Controllers
{
[Route("api/[controller]")]
public class ProductsController : Controller
{
Product[] products = new Product[]
{
new Product { Id = 1, Name = "Tomato Soup", Category = "Groceries", Price = 1 },
new Product { Id = 2, Name = "Yo-yo", Category = "Toys", Price = 3.75M },
new Product { Id = 3, Name = "Hammer", Category = "Hardware", Price = 16.99M }
};

// /api/products
[HttpGet]
public IEnumerable<Product> GetAllProducts()
{
return products;
}

// /api/products/1
[HttpGet("{id}")]
public IActionResult GetProduct(int id)
{
var product = products.FirstOrDefault((p) => p.Id == id);
if (product == null)
{
return NotFound();
}
return Ok(product);
}
}
}

Ahora puede ejecutar el proyecto migrado y vaya a /api/productos; y, debería ver la lista completa de los 3
productos. Vaya a /api/products/1 y debería ver la primera el producto.

Microsoft.AspNetCore.Mvc.WebApiCompatShim
Es una herramienta muy útil cuando ASP.NET Web API migrar proyectos a ASP.NET Core el
Microsoft.AspNetCore.Mvc.WebApiCompatShim biblioteca. La corrección de compatibilidad extiende ASP.NET
Core para permitir a un número de distintas convenciones de Web API 2 para usarse. El ejemplo portar
anteriormente en este documento es bastante básico, por lo que la corrección de compatibilidad no era necesaria.
Para proyectos grandes, usando la corrección de compatibilidad puede ser útil para temporalmente tender API
entre ASP.NET Core y ASP.NET Web API 2.
La corrección de compatibilidad de API Web está pensada para usarse como una medida temporal para facilitar la
migración de grandes proyectos API Web a ASP.NET Core. Con el tiempo, los proyectos deben actualizarse para
usar patrones principales de ASP.NET en lugar de depender de la corrección de compatibilidad.
Entre las características de compatibilidad que se incluyen en Microsoft.AspNetCore.Mvc.WebApiCompatShim se
incluyen:
Agrega un ApiController tipo para que los tipos de base de controladores no tienen que actualizarse.
Habilita el enlace de modelo de estilo Web API. Funciones de enlace del modelo MVC ASP.NET Core de forma
similar a MVC 5, de forma predeterminada. Los cambios de la corrección de compatibilidad del modelo enlace
para que sea más similar a las convenciones de enlace de API Web 2 modelo. Por ejemplo, los tipos complejos
se enlazan automáticamente desde el cuerpo de solicitud.
Extiende el enlace de modelos para que las acciones de controlador pueden tomar parámetros de tipo
HttpRequestMessage .
Agrega los formateadores de mensajes que permite a las acciones para devolver los resultados de tipo
HttpResponseMessage .
Agrega los métodos de respuesta adicionales que pueden usado para atender las respuestas Web API 2
acciones:
Generadores de HttpResponseMessage:
CreateResponse<T>
CreateErrorResponse
Métodos de resultado de acción:
BadRequestErrorMessageResult
ExceptionResult
InternalServerErrorResult
InvalidModelStateResult
NegotiatedContentResult
ResponseMessageResult
Agrega una instancia de IContentNegotiator al contenedor de la aplicación DI y hace contenido tipos
relacionados con la negociación de Microsoft.AspNet.WebApi.Client disponibles. Esto incluye tipos como
DefaultContentNegotiator , MediaTypeFormatter , etcetera.

Para usar la corrección de compatibilidad, debe:


Referencia de la Microsoft.AspNetCore.Mvc.WebApiCompatShim paquete NuGet.
Registrar los servicios de la corrección de compatibilidad con el contenedor de la aplicación DI mediante una
llamada a services.AddWebApiConventions() en la aplicación Startup.ConfigureServices método.
Definir las rutas de Web API específica mediante MapWebApiRoute en el IRouteBuilder en la aplicación
IApplicationBuilder.UseMvc llamar.

Resumen
Migrar un proyecto de ASP.NET Web API simple para MVC de ASP.NET Core es bastante sencilla, gracias a la
compatibilidad integrada con las API Web de MVC de ASP.NET Core. La principal que deben migrar todos los
proyectos de ASP.NET Web API son rutas, los controladores y los modelos, junto con las actualizaciones de los
tipos utilizados por los controladores y acciones.
Migrar la configuración a ASP.NET Core
22/06/2018 • 4 minutes to read • Edit Online

Por Steve Smith y Scott Addie


En el artículo anterior, se comenzó a migrar un proyecto de MVC de ASP.NET a ASP.NET MVC de núcleo. En este
artículo, se migra la configuración.
Vea o descargue el código de ejemplo (cómo descargarlo)

Parámetros de configuración
ASP.NET Core ya no utiliza la Global.asax y web.config archivos que utilizan versiones anteriores de ASP.NET. En
versiones anteriores de ASP.NET, la lógica de inicio de la aplicación se ha puesto en un Application_StartUp
método dentro de Global.asax. Más adelante, en ASP.NET MVC, un Startup.cs archivo se incluyó en la raíz del
proyecto; y, se llamó cuando se inició la aplicación. ASP.NET Core ha adoptado este enfoque por completo
mediante la colocación de toda la lógica de inicio en el Startup.cs archivo.
El web.config archivo también se ha sustituido en ASP.NET Core. Configuración en Sí ahora se puede configurar,
como parte del procedimiento de inicio de aplicación se describen en Startup.cs. Configuración todavía puede usar
los archivos XML, pero normalmente los proyectos de ASP.NET Core colocará los valores de configuración en un
archivo con formato JSON, como appSettings.JSON que se. Sistema de configuración de ASP.NET Core también
es posible tener acceso a las variables de entorno, lo que pueden proporcionar un ubicación más segura y sólida
para valores específicos del entorno. Esto es especialmente cierto para los secretos como cadenas de conexión y
las claves de API que no deben protegerse en el control de código fuente. Vea configuración para obtener más
información sobre la configuración de ASP.NET Core.
En este artículo, estamos comenzando con el proyecto de ASP.NET Core parcialmente migrado desde el artículo
anterior. Para la configuración de la instalación, agregue el siguiente constructor y la propiedad a la Startup.cs
archivo se encuentra en la raíz del proyecto:

public Startup(IConfiguration configuration)


{
Configuration = configuration;
}

public IConfiguration Configuration { get; }

Tenga en cuenta que en este momento, el Startup.cs archivo no se compilará, tal y como necesitamos agregar lo
siguiente using instrucción:

using Microsoft.Extensions.Configuration;

Agregar un appSettings.JSON que se archivo a la raíz del proyecto mediante la plantilla de elemento apropiado:
Migrar la configuración de web.config
Nuestro proyecto de ASP.NET MVC incluye la cadena de conexión de base de datos necesarios en web.config, en
la <connectionStrings> elemento. En el proyecto de ASP.NET Core, vamos a almacenar esta información en el
appSettings.JSON que se archivo. Abra appSettings.JSON que sey tenga en cuenta que ya incluye lo siguiente:

{
"Data": {
"DefaultConnection": {
"ConnectionString": "Server=(localdb)\\MSSQLLocalDB;Database=_CHANGE_ME;Trusted_Connection=True;"
}
}
}

En la línea resaltada descrita anteriormente, cambie el nombre de la base de datos de _CHANGE_ME en el


nombre de la base de datos.

Resumen
ASP.NET Core coloca toda la lógica de inicio de la aplicación en un único archivo, en el que los servicios necesarios
y las dependencias pueden definirse y configuradas. Reemplaza el web.config archivo con una característica de
configuración flexible que puede aprovechar una variedad de formatos de archivo, como JSON, así como las
variables de entorno.
Migrar de autenticación e identidad a ASP.NET Core
22/06/2018 • 5 minutes to read • Edit Online

Por Steve Smith


En el artículo anterior, se migrar configuración desde un proyecto de MVC de ASP.NET a ASP.NET MVC de
núcleo. En este artículo, nos migrar las características de administración de registro, el inicio de sesión y el usuario.

Configure la identidad y pertenencia


En ASP.NET MVC, características de autenticación e identidad se configuran mediante ASP.NET Identity en
Startup.Auth.cs y IdentityConfig.cs, que se encuentra en la App_Start carpeta. En el núcleo de ASP.NET MVC, estas
características se configuran en Startup.cs.
Instalar el Microsoft.AspNetCore.Identity.EntityFrameworkCore y Microsoft.AspNetCore.Authentication.Cookies
paquetes de NuGet.
A continuación, abra Startup.cs y actualizar la Startup.ConfigureServices método se debe utilizar servicios de
Entity Framework y de identidad:

public void ConfigureServices(IServiceCollection services)


{
// Add EF services to the services container.
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();

services.AddMvc();
}

En este momento, hay dos tipos que se hace referencia en el código anterior que hemos aún no hemos migrado
desde el proyecto de MVC de ASP.NET: ApplicationDbContext y ApplicationUser . Crear un nuevo modelos
carpeta en el núcleo de ASP.NET del proyecto y agregarle dos clases correspondientes a estos tipos. Encontrará
ASP.NET MVC versiones de estas clases en /Models/IdentityModels.cs, pero vamos a utilizar un archivo por cada
clase en el proyecto migrado ya que es más nítido.
ApplicationUser.cs:

using Microsoft.AspNetCore.Identity.EntityFrameworkCore;

namespace NewMvcProject.Models
{
public class ApplicationUser : IdentityUser
{
}
}

ApplicationDbContext.cs:
using Microsoft.AspNetCore.Identity.EntityFramework;
using Microsoft.Data.Entity;

namespace NewMvcProject.Models
{
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}

protected override void OnModelCreating(ModelBuilder builder)


{
base.OnModelCreating(builder);
// Customize the ASP.NET Identity model and override the defaults if needed.
// For example, you can rename the ASP.NET Identity table names and more.
// Add your customizations after calling base.OnModelCreating(builder);
}
}
}

El proyecto Web de inicio de MVC de ASP.NET Core no incluye la cantidad personalización de los usuarios, o la
ApplicationDbContext . Al migrar una aplicación real, también necesita migrar todas las propiedades
personalizadas y los métodos de usuario de la aplicación y DbContext clases, así como cualquier otra clase de
modelo que utiliza la aplicación. Por ejemplo, si su DbContext tiene un DbSet<Album> , tiene que migrar la Album
clase.
Con estos archivos en su lugar, el Startup.cs archivo puede hacerse para compilar mediante la actualización de su
using instrucciones:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

Nuestra aplicación ahora está preparado para admitir la autenticación y los servicios de identidad. Basta con que
tienen estas características expuestas a los usuarios.

Migrar la lógica de inicio de sesión y de registro


Con configurado para la aplicación de servicios de identidad y acceso a datos configurado mediante Entity
Framework y SQL Server, estamos listos agregar compatibilidad con inicio de sesión y de registro para la
aplicación. Recuerde que anteriormente en el proceso de migración se comentada una referencia a _LoginPartial
en _Layout.cshtml. Ahora es el momento de volver a ese código, quitar los comentarios y agregar en las vistas
para admitir la funcionalidad de inicio de sesión y los controladores necesarios.
Quite el @Html.Partial línea _Layout.cshtml:

<li>@Html.ActionLink("Contact", "Contact", "Home")</li>


</ul>
@*@Html.Partial("_LoginPartial")*@
</div>
</div>

Ahora, agregue una nueva vista Razor denominada _LoginPartial a la vistas/compartidas carpeta:
Actualización _LoginPartial.cshtml con el código siguiente (reemplazar todo su contenido):

@inject SignInManager<ApplicationUser> SignInManager


@inject UserManager<ApplicationUser> UserManager

@if (SignInManager.IsSignedIn(User))
{
<form asp-area="" asp-controller="Account" asp-action="Logout" method="post" id="logoutForm"
class="navbar-right">
<ul class="nav navbar-nav navbar-right">
<li>
<a asp-area="" asp-controller="Manage" asp-action="Index" title="Manage">Hello
@UserManager.GetUserName(User)!</a>
</li>
<li>
<button type="submit" class="btn btn-link navbar-btn navbar-link">Log out</button>
</li>
</ul>
</form>
}
else
{
<ul class="nav navbar-nav navbar-right">
<li><a asp-area="" asp-controller="Account" asp-action="Register">Register</a></li>
<li><a asp-area="" asp-controller="Account" asp-action="Login">Log in</a></li>
</ul>
}

En este momento, podrá actualizar el sitio en el explorador.

Resumen
ASP.NET Core incluye cambios en las características de ASP.NET Identity. En este artículo, ha visto cómo migrar
las características de administración de autenticación y usuario de identidad de ASP.NET a ASP.NET Core.
Migrar desde ClaimsPrincipal.Current
22/06/2018 • 5 minutes to read • Edit Online

En los proyectos ASP.NET, era habitual usar ClaimsPrincipal.Current recuperar la actual autenticado notificaciones
y la identidad del usuario. En ASP.NET Core, ya no se establece esta propiedad. El código que se según lo debe
actualizarse para obtener la identidad del usuario autenticado actual a través de un medio diferente.

Datos específicos del contexto en lugar de datos estáticos


Cuando se usa ASP.NET Core, los valores de ambos ClaimsPrincipal.Current y Thread.CurrentPrincipal no están
establecidos. Estas propiedades representan el estado estático, que suele evitar el núcleo de ASP.NET. En su lugar,
arquitectura de ASP.NET Core consiste en recuperar las dependencias (como la identidad del usuario actual) de las
colecciones de servicio específicas del contexto (mediante su inyección de dependencia modelo (DI)). ¿Qué es más,
Thread.CurrentPrincipal es subproceso estático, por lo que no pueden conservar cambios en algunos escenarios
asincrónicos (y ClaimsPrincipal.Current simplemente llama Thread.CurrentPrincipal de forma predeterminada).
Para entender a los tipos de subprocesos de problemas pueden provocar que los miembros estáticos en escenarios
asincrónicos, considere el siguiente fragmento de código:

// Create a ClaimsPrincipal and set Thread.CurrentPrincipal


var identity = new ClaimsIdentity();
identity.AddClaim(new Claim(ClaimTypes.Name, "User1"));
Thread.CurrentPrincipal = new ClaimsPrincipal(identity);

// Check the current user


Console.WriteLine($"Current user: {Thread.CurrentPrincipal?.Identity.Name}");

// For the method to complete asynchronously


await Task.Yield();

// Check the current user after


Console.WriteLine($"Current user: {Thread.CurrentPrincipal?.Identity.Name}");

El código de ejemplo anterior se establece Thread.CurrentPrincipal y comprueba su valor antes y después esperar
una llamada asincrónica. Thread.CurrentPrincipal es específico de la subproceso en el que se establece y el
método es apropiado reanudar la ejecución en un subproceso diferente después de la instrucción await. Por lo
tanto, Thread.CurrentPrincipal está presente cuando se comprueba en primer lugar, pero es null después de
llamar a await Task.Yield() .
Obtener la identidad del usuario actual de recopilación de la aplicación de servicio DI es más comprobable,
demasiado, ya que las identidades de prueba se pueden insertar fácilmente.

Recuperar el usuario actual en una aplicación de ASP.NET Core


Existen varias opciones para la recuperación del usuario autenticado actual ClaimsPrincipal en ASP.NET Core en
lugar de ClaimsPrincipal.Current :
ControllerBase.User. Controladores MVC pueden tener acceso el usuario autenticado actual con sus
usuario propiedad.
HttpContext.User. Componentes con acceso a la actual HttpContext (software intermedio, por ejemplo)
puede obtener el usuario actual ClaimsPrincipal de HttpContext.User.
Pasa del autor de llamada. Las bibliotecas no tienen acceso a la actual HttpContext a menudo se
denominan de controladores o componentes de middleware y puede tener la identidad del usuario actual
que se pasa como argumento.
IHttpContextAccessor. El proyecto ASP.NET que se está migrando a ASP.NET Core puede ser demasiado
grande para pasar fácilmente la identidad del usuario actual para todas las ubicaciones necesarias. En tales
casos, IHttpContextAccessor puede utilizarse para solucionar este problema. IHttpContextAccessor puede
tener acceso a la actual HttpContext (si existe). Una solución a corto plazo para obtener la identidad del
usuario actual en el código que no se ha actualizado para trabajar con la arquitectura orientada a DI de
ASP.NET Core sería:
Asegúrese de IHttpContextAccessor disponibles en el contenedor de DI mediante una llamada a
AddHttpContextAccessor en Startup.ConfigureServices .
Obtener una instancia de IHttpContextAccessor durante el inicio y almacenarlo en una variable estática.
La instancia se hace disponible para código que anteriormente estaba recuperando el usuario actual de
una propiedad estática.
Recuperar el usuario actual ClaimsPrincipal con HttpContextAccessor.HttpContext?.User . Si este código
se utiliza fuera del contexto de una solicitud HTTP, el HttpContext es null.

La última opción, mediante IHttpContextAccessor , es contraria a principios de ASP.NET Core (por lo que prefieren
dependencias insertadas dependencias estáticas). Previsto eliminar eventualmente la dependencia en el método
estático IHttpContextAccessor auxiliar. Puede ser un puente útil, sin embargo, al migrar grandes aplicaciones
ASP.NET existentes que anteriormente estaban usando ClaimsPrincipal.Current .
Migrar de autenticación de pertenencia a ASP.NET a
ASP.NET Core 2.0 Identity
22/06/2018 • 11 minutes to read • Edit Online

Por Isaac Levin


Este artículo muestra cómo migrar el esquema de base de datos para las aplicaciones ASP.NET mediante la
autenticación de pertenencia a ASP.NET Core 2.0 Identity.

NOTE
Este documento proporciona los pasos necesarios para migrar el esquema de base de datos para aplicaciones basadas en la
pertenencia a ASP.NET para el esquema de base de datos usado para identidad de núcleo de ASP.NET. Para obtener más
información sobre cómo migrar de autenticación basada en pertenencia a ASP.NET a ASP.NET Identity, consulte migrar una
aplicación existente de SQL pertenencia a ASP.NET Identity. Para obtener más información acerca de la identidad de núcleo de
ASP.NET, vea Introducción a la identidad en ASP.NET Core.

Revisión del esquema de pertenencia


Antes de ASP.NET 2.0, los desarrolladores se encargan que cree el proceso de autenticación y autorización
completo para sus aplicaciones. Con ASP.NET 2.0, se introdujo pertenencia, proporcionar una solución reutilizable
para administrar la seguridad en las aplicaciones ASP.NET. Los desarrolladores ahora podían arrancar un esquema
en una base de datos de SQL Server con el aspnet_regsql.exe comando. Después de ejecutar este comando, en las
siguientes tablas se crearon en la base de datos.

Para migrar aplicaciones existentes a ASP.NET Core 2.0 Identity, los datos en estas tablas tienen que migrarse a las
tablas utilizadas por el nuevo esquema de identidad.

Esquema de núcleo identidad 2.0 de ASP.NET


Núcleo de ASP.NET 2.0 sigue la identidad principio introducida en ASP.NET 4.5. Aunque se comparte el principio,
la implementación entre los marcos es diferente, incluso entre las versiones de ASP.NET Core (vea migrar
autenticación e identidad a ASP.NET Core 2.0).
La manera más rápida de ver el esquema de núcleo de ASP.NET 2.0 Identity consiste en crear una nueva aplicación
de ASP.NET Core 2.0. Siga estos pasos en Visual Studio de 2017:
Seleccione Archivo > Nuevo > Proyecto.
Crear un nuevo aplicación Web de ASP.NET Corey el nombre del proyecto CoreIdentitySample.
Seleccione ASP.NET Core 2.0 en la lista desplegable y, luego, seleccione Aplicación web. Esta plantilla genera
un páginas de Razor aplicación. Antes de hacer clic Aceptar, haga clic en Cambiar autenticación.
Elija cuentas de usuario individuales para las plantillas de identidad. Por último, haga clic en Aceptar, a
continuación, Aceptar. Visual Studio crea un proyecto mediante la plantilla de ASP.NET Core Identity.
Identidad de núcleo de ASP.NET 2.0 utiliza Entity Framework Core para interactuar con la base de datos almacena
los datos de autenticación. En orden de la aplicación recién creada para que funcione, debe ser una base de datos
para almacenar estos datos. Después de crear una nueva aplicación, la manera más rápida para inspeccionar el
esquema en un entorno de base de datos es crear la base de datos usando migraciones de Entity Framework. Este
proceso crea una base de datos, ya sea localmente o en otro lugar, que imita ese esquema. Revise la
documentación anterior para obtener más información.
Para crear una base de datos con el esquema de la identidad de ASP.NET Core, ejecute el Update-Database en
Visual Studio Package Manager Console ventana (PMC )—se encuentra en herramientas > Administrador de
paquetes de NuGet > consola de administrador de paquetes. PMC admite la ejecución de los comandos de
Entity Framework.
Comandos de Entity Framework usan la cadena de conexión para la base de datos especificada en
appSettings.JSON que se. La siguiente cadena de conexión tiene como destino una base de datos en localhost
denominado asp -net-core-identity. En esta configuración, Entity Framework está configurado para usar el
DefaultConnection cadena de conexión.

{
"ConnectionStrings": {
"DefaultConnection": "Server=localhost;Database=aspnet-core-
identity;Trusted_Connection=True;MultipleActiveResultSets=true"
}
}

Este comando crea la base de datos especificada con el esquema y los datos necesarios para la inicialización de la
aplicación. La siguiente imagen muestra la estructura de tabla que se crea con los pasos anteriores.

migrar el esquema
Existen diferencias sutiles en los campos para la suscripción y ASP.NET Core Identity y estructuras de tabla. El
modelo ha cambiado considerablemente para la autenticación/autorización con aplicaciones ASP.NET y ASP.NET
Core. Los objetos de clave que todavía se utilizan con identidad están usuarios y Roles. Estas son las tablas de
asignación de usuarios, Roles, y UserRoles.
Usuarios
MEMBERSHIP(ASPNET_USERS/A
IDENTITY(ASPNETUSERS) SPNET_MEMBERSHIP)

Nombre de campo Type Nombre de campo Type

Id cadena aspnet_Users.UserId cadena

UserName cadena aspnet_Users.UserName cadena

Email cadena aspnet_Membership.Email cadena

NormalizedUserName cadena aspnet_Users.LoweredUserName cadena

NormalizedEmail cadena cadena


aspnet_Membership.LoweredEmail

PhoneNumber cadena aspnet_Users.MobileAlias cadena

LockoutEnabled bits aspnet_Membership.IsLockedOutbits

NOTE
No todas las asignaciones de campo son similares a las relaciones uno a uno de pertenencia a ASP.NET Core Identity. La tabla
anterior toma el esquema de usuario de pertenencia predeterminado y lo asigna al esquema de núcleo de ASP.NET Identity.
Los campos personalizados que se utilizaron para la suscripción es necesario asignar manualmente. En esta asignación, no
hay ninguna asignación para las contraseñas, como criterios de contraseña y Sales de la contraseña no se migran entre los
dos. Se recomienda dejar la contraseña como null y pedir a los usuarios restablecer sus contraseñas. En ASP.NET
Core Identity, LockoutEnd debe establecerse en una fecha en el futuro si el usuario está bloqueado. Esto se muestra en el
script de migración.

Roles
IDENTITY(ASPNETROLES) MEMBERSHIP(ASPNET_ROLES)

Nombre de campo Type Nombre de campo Type

Id cadena RoleId cadena

Name cadena RoleName cadena

NormalizedName cadena LoweredRoleName cadena

Roles de usuario
MEMBERSHIP(ASPNET_USERSI
IDENTITY(ASPNETUSERROLES) NROLES)

Nombre de campo Type Nombre de campo Type

RoleId cadena RoleId cadena

UserId cadena UserId cadena


Hacer referencia a las tablas de asignación anterior al crear un script de migración para usuarios y Roles. En el
siguiente ejemplo se da por supuesto que tiene dos bases de datos en un servidor de base de datos. Una base de
datos contiene el esquema de pertenencia a ASP.NET existente y los datos. La otra base de datos se creó mediante
los pasos descritos anteriormente. Los comentarios son incluido en línea para obtener más detalles.

-- THIS SCRIPT NEEDS TO RUN FROM THE CONTEXT OF THE MEMBERSHIP DB


BEGIN TRANSACTION MigrateUsersAndRoles
use aspnetdb

-- INSERT USERS
INSERT INTO coreidentity.dbo.aspnetusers
(id,
username,
normalizedusername,
passwordhash,
securitystamp,
emailconfirmed,
phonenumber,
phonenumberconfirmed,
twofactorenabled,
lockoutend,
lockoutenabled,
accessfailedcount,
email,
normalizedemail)
SELECT aspnet_users.userid,
aspnet_users.username,
aspnet_users.loweredusername,
--Creates an empty password since passwords don't map between the two schemas
'',
--Security Stamp is a token used to verify the state of an account and is subject to change at any
time. It should be intialized as a new ID.
NewID(),
--EmailConfirmed is set when a new user is created and confirmed via email. Users must have this set
during migration to ensure they're able to reset passwords.
1,
aspnet_users.mobilealias,
CASE
WHEN aspnet_Users.MobileAlias is null THEN 0
ELSE 1
END,
--2-factor Auth likely wasn't setup in Membership for users, so setting as false.
0,
CASE
--Setting lockout date to time in the future (1000 years)
WHEN aspnet_membership.islockedout = 1 THEN Dateadd(year, 1000,
Sysutcdatetime())
ELSE NULL
END,
aspnet_membership.islockedout,
--AccessFailedAccount is used to track failed logins. This is stored in membership in multiple columns.
Setting to 0 arbitrarily.
0,
aspnet_membership.email,
aspnet_membership.loweredemail
FROM aspnet_users
LEFT OUTER JOIN aspnet_membership
ON aspnet_membership.applicationid =
aspnet_users.applicationid
AND aspnet_users.userid = aspnet_membership.userid
LEFT OUTER JOIN coreidentity.dbo.aspnetusers
ON aspnet_membership.userid = aspnetusers.id
WHERE aspnetusers.id IS NULL

-- INSERT ROLES
INSERT INTO coreIdentity.dbo.aspnetroles(id,name)
SELECT roleId,rolename
SELECT roleId,rolename
FROM aspnet_roles;

-- INSERT USER ROLES


INSERT INTO coreidentity.dbo.aspnetuserroles(userid,roleid)
SELECT userid,roleid
FROM aspnet_usersinroles;

IF @@ERROR <> 0
BEGIN
ROLLBACK TRANSACTION MigrateUsersAndRoles
RETURN
END

COMMIT TRANSACTION MigrateUsersAndRoles

Tras finalizar este script, la aplicación de ASP.NET Core Identity que se creó anteriormente se rellena con los
usuarios de pertenencia. Los usuarios deben cambiar sus contraseñas antes de iniciar sesión.

NOTE
Si el sistema de pertenencia tenía usuarios con nombres de usuario que no coincidía con su dirección de correo electrónico,
se requiere para la aplicación que creó anteriormente para dar cabida a este cambio. La plantilla predeterminada espera
UserName y Email para que coincidan. En situaciones en las que son diferentes, es necesario modificar para usar el
proceso de inicio de sesión UserName en lugar de Email .

En el PageModel de la página de inicio de sesión, ubicado en Pages\Account\Login.cshtml.cs, quite el


[EmailAddress] de atributo de la correo electrónico propiedad. Cambie su nombre a nombre de usuario. Esto
requiere un cambio siempre que sea EmailAddress se ha mencionado, en la vista y PageModel. El resultado es
similar a lo siguiente:

Pasos siguientes
En este tutorial, aprendió cómo trasladar a los usuarios de pertenencia SQL ASP.NET Core 2.0 Identity. Para
obtener más información sobre ASP.NET Core Identity, consulte Introducción a la identidad.
Migrar controladores HTTP y módulos ASP.NET
Core middleware
22/06/2018 • 26 minutes to read • Edit Online

Por Matt Perdeck


Este artículo muestra cómo migrar ASP.NET existente módulos HTTP y controladores de system.webserver a
ASP.NET Core middleware.

Módulos y controladores revisan


Antes de proceder con middleware de ASP.NET Core, primero Resumamos cómo funcionan los controladores y
módulos HTTP:

Los controladores son:


Las clases que implementan IHttpHandler
Utilizado para controlar solicitudes con un nombre de archivo especificado o una extensión, como
informes
Configurar en Web.config
Los módulos son:
Las clases que implementan IHttpModule
Se invoca para cada solicitud
Capaz de cortocircuito (detener el procesamiento de una solicitud)
Puede agregar a la respuesta HTTP, o crear sus propios
Configurar en Web.config
El orden en el que los módulos de procesan las solicitudes entrantes viene determinado por:
1. El ciclo de vida de aplicación, que es un eventos serie desencadenados por ASP.NET: BeginRequest,
AuthenticateRequest, etcetera. Cada módulo puede crear un controlador para uno o varios eventos.
2. Para el mismo evento, el orden en el que está configurados en Web.config.
Además de los módulos, puede agregar controladores para los eventos de ciclo de vida a sus Global.asax.cs
archivo. Estos controladores se ejecutan después de los controladores en los módulos configurados.

De controladores y módulos middleware


Middleware son más sencillas que controladores y módulos HTTP:
Módulos, controladores, Global.asax.cs, Web.config (excepto para la configuración de IIS ) y el ciclo de vida
de la aplicación han desaparecido
Los roles de los módulos y los controladores se hayan realizado el middleware
Middleware se configuran mediante código en lugar de en Web.config
Bifurcación de canalización le permite enviar las solicitudes de middleware específico, según no solo la
dirección URL sino también en los encabezados de solicitud, las cadenas de consulta, etcetera.
Middleware son muy similares a los módulos:
Se invoca en la entidad de seguridad para todas las solicitudes
Capaz de una solicitud de cortocircuito por no pasar la solicitud al siguiente middleware
Puede crear su propia respuesta HTTP
El software intermedio y módulos se procesan en un orden diferente:
Orden de middleware se basa en el orden en el que se insertan en la canalización de solicitudes, mientras
que el orden de los módulos se basa principalmente en ciclo de vida de aplicación eventos
Orden de middleware para las respuestas es el inverso de la para las solicitudes, mientras que el orden de
los módulos es el mismo para las solicitudes y respuestas
Vea crear una canalización de middleware con IApplicationBuilder

Tenga en cuenta cómo en la imagen anterior, el middleware de autenticación había cortocircuitado la solicitud.

Migrar código del módulo middleware


Un módulo HTTP existente tendrá un aspecto similar al siguiente:

// ASP.NET 4 module

using System;
using System.Web;

namespace MyApp.Modules
{
public class MyModule : IHttpModule
{
public void Dispose()
{
}

public void Init(HttpApplication application)


{
application.BeginRequest += (new EventHandler(this.Application_BeginRequest));
application.EndRequest += (new EventHandler(this.Application_EndRequest));
}

private void Application_BeginRequest(Object source, EventArgs e)


{
HttpContext context = ((HttpApplication)source).Context;

// Do something with context near the beginning of request processing.


}

private void Application_EndRequest(Object source, EventArgs e)


{
HttpContext context = ((HttpApplication)source).Context;

// Do something with context near the end of request processing.


}
}
}

Como se muestra en el Middleware página, un middleware de ASP.NET Core es una clase que expone un
Invoke tomar método un HttpContext y devolver un Task . El middleware de nuevo tendrá un aspecto similar al
siguiente:
// ASP.NET Core middleware

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using System.Threading.Tasks;

namespace MyApp.Middleware
{
public class MyMiddleware
{
private readonly RequestDelegate _next;

public MyMiddleware(RequestDelegate next)


{
_next = next;
}

public async Task Invoke(HttpContext context)


{
// Do something with context near the beginning of request processing.

await _next.Invoke(context);

// Clean up.
}
}

public static class MyMiddlewareExtensions


{
public static IApplicationBuilder UseMyMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<MyMiddleware>();
}
}
}

La plantilla de middleware anterior se realizó desde la sección de escribir middleware.


El MyMiddlewareExtensions clase auxiliar resulta más fácil de configurar el middleware en su Startup clase. El
UseMyMiddleware método agrega la clase de middleware a la canalización de solicitudes. Servicios requeridos por
el middleware obtengan insertados en el constructor del middleware.
El módulo podría terminar una solicitud, por ejemplo, si el usuario no autorizado:

// ASP.NET 4 module that may terminate the request

private void Application_BeginRequest(Object source, EventArgs e)


{
HttpContext context = ((HttpApplication)source).Context;

// Do something with context near the beginning of request processing.

if (TerminateRequest())
{
context.Response.End();
return;
}
}

Un middleware encarga de ello llamando no Invoke en el siguiente middleware en la canalización. Tenga en


cuenta que esto no finaliza completamente la solicitud, porque middlewares anterior aún se invocará cuando la
respuesta realiza su forma de volver a través de la canalización.
// ASP.NET Core middleware that may terminate the request

public async Task Invoke(HttpContext context)


{
// Do something with context near the beginning of request processing.

if (!TerminateRequest())
await _next.Invoke(context);

// Clean up.
}

Cuando se migra la funcionalidad de su módulo el middleware de nuevo, es posible que el código no se compila
porque la HttpContext clase ha cambiado significativamente en ASP.NET Core. Más adelante en, verá cómo
migrar a la nueva HttpContext principales de ASP.NET.

Migrar inserción de módulo en la canalización de solicitud


Módulos HTTP normalmente se agregan a la canalización de solicitud con Web.config:

<?xml version="1.0" encoding="utf-8"?>


<!--ASP.NET 4 web.config-->
<configuration>
<system.webServer>
<modules>
<add name="MyModule" type="MyApp.Modules.MyModule"/>
</modules>
</system.webServer>
</configuration>

Convertir esto por agregar su middleware nueva a la canalización de solicitud en su Startup clase:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();

if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}

app.UseMyMiddleware();

app.UseMyMiddlewareWithParams();

var myMiddlewareOptions = Configuration.GetSection("MyMiddlewareOptionsSection").Get<MyMiddlewareOptions>


();
var myMiddlewareOptions2 =
Configuration.GetSection("MyMiddlewareOptionsSection2").Get<MyMiddlewareOptions>();
app.UseMyMiddlewareWithParams(myMiddlewareOptions);
app.UseMyMiddlewareWithParams(myMiddlewareOptions2);

app.UseMyTerminatingMiddleware();

// Create branch to the MyHandlerMiddleware.


// All requests ending in .report will follow this branch.
app.MapWhen(
context => context.Request.Path.ToString().EndsWith(".report"),
appBranch => {
// ... optionally add more middleware to this branch
appBranch.UseMyHandler();
});

app.MapWhen(
context => context.Request.Path.ToString().EndsWith(".context"),
appBranch => {
appBranch.UseHttpContextDemoMiddleware();
});

app.UseStaticFiles();

app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}

El lugar exacto de la canalización donde insertar el middleware nueva depende de los eventos que tratan como
un módulo ( BeginRequest , EndRequest , etc.) y su orden en la lista de módulos de Web.config.
Como anteriormente indicó, no hay ningún ciclo de vida de la aplicación de ASP.NET Core y el orden en que se
procesan las respuestas middleware distinto del orden usado por módulos. Esto podría tomar una decisión de
ordenación más complejo.
Si la ordenación se convierte en un problema, puede dividir el módulo de varios componentes de software
intermedio que se pueden ordenar de forma independiente.

Migrar código del controlador de middleware


Un controlador HTTP es algo parecido a esto:

// ASP.NET 4 handler

using System.Web;

namespace MyApp.HttpHandlers
{
public class MyHandler : IHttpHandler
{
public bool IsReusable { get { return true; } }

public void ProcessRequest(HttpContext context)


{
string response = GenerateResponse(context);

context.Response.ContentType = GetContentType();
context.Response.Output.Write(response);
}

// ...

private string GenerateResponse(HttpContext context)


{
string title = context.Request.QueryString["title"];
return string.Format("Title of the report: {0}", title);
}

private string GetContentType()


{
return "text/plain";
}
}
}

En el proyecto de ASP.NET Core, se traduciría en un middleware similar al siguiente:


// ASP.NET Core middleware migrated from a handler

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using System.Threading.Tasks;

namespace MyApp.Middleware
{
public class MyHandlerMiddleware
{

// Must have constructor with this signature, otherwise exception at run time
public MyHandlerMiddleware(RequestDelegate next)
{
// This is an HTTP Handler, so no need to store next
}

public async Task Invoke(HttpContext context)


{
string response = GenerateResponse(context);

context.Response.ContentType = GetContentType();
await context.Response.WriteAsync(response);
}

// ...

private string GenerateResponse(HttpContext context)


{
string title = context.Request.Query["title"];
return string.Format("Title of the report: {0}", title);
}

private string GetContentType()


{
return "text/plain";
}
}

public static class MyHandlerExtensions


{
public static IApplicationBuilder UseMyHandler(this IApplicationBuilder builder)
{
return builder.UseMiddleware<MyHandlerMiddleware>();
}
}
}

Este middleware es muy similar del middleware correspondiente a los módulos. La única diferencia es que aquí
no hay ninguna llamada a _next.Invoke(context) . Que tiene sentido, porque el controlador no es el final de la
canalización de solicitud, por lo que no habrá ningún siguiente middleware para invocar.

Migrar inserción de controlador en la canalización de solicitud


La configuración de un controlador HTTP se realiza Web.config y es algo parecido a esto:
<?xml version="1.0" encoding="utf-8"?>
<!--ASP.NET 4 web.config-->
<configuration>
<system.webServer>
<handlers>
<add name="MyHandler" verb="*" path="*.report" type="MyApp.HttpHandlers.MyHandler"
resourceType="Unspecified" preCondition="integratedMode"/>
</handlers>
</system.webServer>
</configuration>

Puede convertir este agregando el middleware de controlador nuevo a la canalización de solicitud en su Startup
(clase), similar a middleware convertido a partir de los módulos. El problema con este enfoque es que enviaría
todas las solicitudes para el middleware de controlador nuevo. Sin embargo, sólo desea que las solicitudes con
una extensión específica para alcanzar el middleware. Esto le proporcionaría la misma funcionalidad que no
tenga el controlador HTTP.
Una solución es crear una bifurcación de la canalización de solicitudes con una extensión determinada, mediante
el MapWhen método de extensión. Para ello, en el mismo Configure método donde se agrega el middleware de
otro:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();

if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}

app.UseMyMiddleware();

app.UseMyMiddlewareWithParams();

var myMiddlewareOptions = Configuration.GetSection("MyMiddlewareOptionsSection").Get<MyMiddlewareOptions>


();
var myMiddlewareOptions2 =
Configuration.GetSection("MyMiddlewareOptionsSection2").Get<MyMiddlewareOptions>();
app.UseMyMiddlewareWithParams(myMiddlewareOptions);
app.UseMyMiddlewareWithParams(myMiddlewareOptions2);

app.UseMyTerminatingMiddleware();

// Create branch to the MyHandlerMiddleware.


// All requests ending in .report will follow this branch.
app.MapWhen(
context => context.Request.Path.ToString().EndsWith(".report"),
appBranch => {
// ... optionally add more middleware to this branch
appBranch.UseMyHandler();
});

app.MapWhen(
context => context.Request.Path.ToString().EndsWith(".context"),
appBranch => {
appBranch.UseHttpContextDemoMiddleware();
});

app.UseStaticFiles();

app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}

MapWhen toma estos parámetros:


1. Una expresión lambda que toma el HttpContext y devuelve true si la solicitud debe pasar a la rama. Esto
significa que puede crear una bifurcación de solicitudes no solo en función de su extensión, sino también
en los encabezados de solicitud, los parámetros de cadena de consulta, etcetera.
2. Una expresión lambda que toma un IApplicationBuilder y agrega el software intermedio para la
bifurcación. Esto significa que puede agregar middleware adicional a la bifurcación delante el middleware
de controlador.
Software intermedio se agrega a la canalización antes de que se invocará la bifurcación en todas las solicitudes; la
bifurcación no tendrá ningún impacto en ellos.

Opciones de middleware usando el patrón de opciones de carga


Algunos controladores y módulos tienen opciones de configuración que se almacenan en Web.config. Sin
embargo, en ASP.NET Core un nuevo modelo de configuración se utiliza en lugar de Web.config.
El nuevo sistema de configuración ofrece las siguientes opciones para resolver este problema:
Insertar directamente las opciones en el middleware, como se muestra en el próxima sección.
Use la patrón opciones:
1. Cree una clase para contener las opciones de middleware, por ejemplo:

public class MyMiddlewareOptions


{
public string Param1 { get; set; }
public string Param2 { get; set; }
}

2. Almacenar los valores de opción


El sistema de configuración le permite almacenar valores de opción en cualquier lugar que desee. Sin
embargo, los sitios más uso appSettings.JSON que se, por lo que le llevaremos a ese método:

{
"MyMiddlewareOptionsSection": {
"Param1": "Param1Value",
"Param2": "Param2Value"
}
}

MyMiddlewareOptionsSection aquí es un nombre de sección. No tiene que ser el mismo que el nombre de
la clase de opciones.
3. Asociar a los valores de opción de la clase de opciones
El patrón de opciones usa framework de inyección de dependencia de ASP.NET Core para asociar el tipo
de opciones (como MyMiddlewareOptions ) con un MyMiddlewareOptions objeto que tiene las opciones
reales.
Actualización de su Startup clase:
a. Si usas appSettings.JSON que se, agregar para el generador de configuración de la Startup
constructor:

public Startup(IHostingEnvironment env)


{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
Configuration = builder.Build();
}

b. Configurar el servicio de opciones:


public void ConfigureServices(IServiceCollection services)
{
// Setup options service
services.AddOptions();

// Load options from section "MyMiddlewareOptionsSection"


services.Configure<MyMiddlewareOptions>(
Configuration.GetSection("MyMiddlewareOptionsSection"));

// Add framework services.


services.AddMvc();
}

c. Asocie las opciones de la clase de opciones:

public void ConfigureServices(IServiceCollection services)


{
// Setup options service
services.AddOptions();

// Load options from section "MyMiddlewareOptionsSection"


services.Configure<MyMiddlewareOptions>(
Configuration.GetSection("MyMiddlewareOptionsSection"));

// Add framework services.


services.AddMvc();
}

4. Insertar las opciones en el constructor de middleware. Esto es similar a insertar opciones en un


controlador.

public class MyMiddlewareWithParams


{
private readonly RequestDelegate _next;
private readonly MyMiddlewareOptions _myMiddlewareOptions;

public MyMiddlewareWithParams(RequestDelegate next,


IOptions<MyMiddlewareOptions> optionsAccessor)
{
_next = next;
_myMiddlewareOptions = optionsAccessor.Value;
}

public async Task Invoke(HttpContext context)


{
// Do something with context near the beginning of request processing
// using configuration in _myMiddlewareOptions

await _next.Invoke(context);

// Do something with context near the end of request processing


// using configuration in _myMiddlewareOptions
}
}

El UseMiddleware método de extensión que agrega el middleware para la IApplicationBuilder se


encarga de inserción de dependencias.
Esto no se limita a IOptions objetos. Puede insertar cualquier otro objeto que requiere el middleware de
esta manera.
Opciones de middleware a través de inyección directa de carga
El patrón de opciones tiene la ventaja de que crea un acoplamiento entre los valores de las opciones y sus
consumidores flexible. Una vez que haya asociado una clase de opciones con los valores de opciones real,
cualquier otra clase puede obtener acceso a las opciones a través del marco de inyección de dependencia. No es
necesario pasar valores de opciones.
Este modo se divide aunque si desea utilizar el mismo middleware dos veces, con diferentes opciones. Por
ejemplo un middleware de autorización utilizado en distintas ramas que permite a distintos roles. No se puede
asociar dos objetos diferentes opciones con la clase de uno opciones.
La solución consiste en obtener los objetos de opciones con los valores de opciones real en su Startup clase y
las pasará directamente a cada instancia de su middleware.
1. Agregar una segunda clave para appSettings.JSON que se
Para agregar un segundo conjunto de opciones para la appSettings.JSON que se de archivos, use una
nueva clave para identificar de forma exclusiva:

{
"MyMiddlewareOptionsSection2": {
"Param1": "Param1Value2",
"Param2": "Param2Value2"
},
"MyMiddlewareOptionsSection": {
"Param1": "Param1Value",
"Param2": "Param2Value"
}
}

2. Recuperar valores de las opciones y pasarlos a middleware. El Use... método de extensión (que agrega el
middleware a la canalización) es un punto lógico para pasar los valores de opción:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();

if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}

app.UseMyMiddleware();

app.UseMyMiddlewareWithParams();

var myMiddlewareOptions =
Configuration.GetSection("MyMiddlewareOptionsSection").Get<MyMiddlewareOptions>();
var myMiddlewareOptions2 =
Configuration.GetSection("MyMiddlewareOptionsSection2").Get<MyMiddlewareOptions>();
app.UseMyMiddlewareWithParams(myMiddlewareOptions);
app.UseMyMiddlewareWithParams(myMiddlewareOptions2);

app.UseMyTerminatingMiddleware();

// Create branch to the MyHandlerMiddleware.


// All requests ending in .report will follow this branch.
app.MapWhen(
context => context.Request.Path.ToString().EndsWith(".report"),
appBranch => {
// ... optionally add more middleware to this branch
appBranch.UseMyHandler();
});

app.MapWhen(
context => context.Request.Path.ToString().EndsWith(".context"),
appBranch => {
appBranch.UseHttpContextDemoMiddleware();
});

app.UseStaticFiles();

app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}

3. Habilita el middleware tomar un parámetro de opciones. Proporcionar una sobrecarga de la Use...


método de extensión (que toma el parámetro options y lo pasa a UseMiddleware ). Cuando UseMiddleware
se llama con parámetros, pasa los parámetros al constructor de middleware cuando crea una instancia del
objeto de middleware.
public static class MyMiddlewareWithParamsExtensions
{
public static IApplicationBuilder UseMyMiddlewareWithParams(
this IApplicationBuilder builder)
{
return builder.UseMiddleware<MyMiddlewareWithParams>();
}

public static IApplicationBuilder UseMyMiddlewareWithParams(


this IApplicationBuilder builder, MyMiddlewareOptions myMiddlewareOptions)
{
return builder.UseMiddleware<MyMiddlewareWithParams>(
new OptionsWrapper<MyMiddlewareOptions>(myMiddlewareOptions));
}
}

Tenga en cuenta cómo se ajusta el objeto de opciones en un OptionsWrapper objeto. Esto implementa
IOptions , tal y como se esperaba el constructor de middleware.

Migrar a la nueva HttpContext


Se ha visto anteriormente que el Invoke método en el middleware toma un parámetro de tipo HttpContext :

public async Task Invoke(HttpContext context)

HttpContext ha cambiado significativamente en ASP.NET Core. Esta sección muestra cómo traducir las
propiedades más utilizadas de System.Web.HttpContext al nuevo Microsoft.AspNetCore.Http.HttpContext .
HttpContext
HttpContext.Items se convierte en:

IDictionary<object, object> items = httpContext.Items;

Identificador único de la solicitud (ningún homólogo System.Web.HttpContext)


Proporciona un identificador único para cada solicitud. Resulta muy útil para incluir en los registros.

string requestId = httpContext.TraceIdentifier;

HttpContext.Request
HttpContext.Request.HttpMethod se convierte en:

string httpMethod = httpContext.Request.Method;

HttpContext.Request.QueryString se convierte en:


IQueryCollection queryParameters = httpContext.Request.Query;

// If no query parameter "key" used, values will have 0 items


// If single value used for a key (...?key=v1), values will have 1 item ("v1")
// If key has multiple values (...?key=v1&key=v2), values will have 2 items ("v1" and "v2")
IList<string> values = queryParameters["key"];

// If no query parameter "key" used, value will be ""


// If single value used for a key (...?key=v1), value will be "v1"
// If key has multiple values (...?key=v1&key=v2), value will be "v1,v2"
string value = queryParameters["key"].ToString();

HttpContext.Request.Url y HttpContext.Request.RawUrl traducir al:

// using Microsoft.AspNetCore.Http.Extensions;
var url = httpContext.Request.GetDisplayUrl();

HttpContext.Request.IsSecureConnection se convierte en:

var isSecureConnection = httpContext.Request.IsHttps;

HttpContext.Request.UserHostAddress se convierte en:

var userHostAddress = httpContext.Connection.RemoteIpAddress?.ToString();

HttpContext.Request.Cookies se convierte en:

IRequestCookieCollection cookies = httpContext.Request.Cookies;


string unknownCookieValue = cookies["unknownCookie"]; // will be null (no exception)
string knownCookieValue = cookies["cookie1name"]; // will be actual value

HttpContext.Request.RequestContext.RouteData se convierte en:

var routeValue = httpContext.GetRouteValue("key");

HttpContext.Request.Headers se convierte en:

// using Microsoft.AspNetCore.Http.Headers;
// using Microsoft.Net.Http.Headers;

IHeaderDictionary headersDictionary = httpContext.Request.Headers;

// GetTypedHeaders extension method provides strongly typed access to many headers


var requestHeaders = httpContext.Request.GetTypedHeaders();
CacheControlHeaderValue cacheControlHeaderValue = requestHeaders.CacheControl;

// For unknown header, unknownheaderValues has zero items and unknownheaderValue is ""
IList<string> unknownheaderValues = headersDictionary["unknownheader"];
string unknownheaderValue = headersDictionary["unknownheader"].ToString();

// For known header, knownheaderValues has 1 item and knownheaderValue is the value
IList<string> knownheaderValues = headersDictionary[HeaderNames.AcceptLanguage];
string knownheaderValue = headersDictionary[HeaderNames.AcceptLanguage].ToString();

HttpContext.Request.UserAgent se convierte en:


string userAgent = headersDictionary[HeaderNames.UserAgent].ToString();

HttpContext.Request.UrlReferrer se convierte en:

string urlReferrer = headersDictionary[HeaderNames.Referer].ToString();

HttpContext.Request.ContentType se convierte en:

// using Microsoft.Net.Http.Headers;

MediaTypeHeaderValue mediaHeaderValue = requestHeaders.ContentType;


string contentType = mediaHeaderValue?.MediaType.ToString(); // ex. application/x-www-form-urlencoded
string contentMainType = mediaHeaderValue?.Type.ToString(); // ex. application
string contentSubType = mediaHeaderValue?.SubType.ToString(); // ex. x-www-form-urlencoded

System.Text.Encoding requestEncoding = mediaHeaderValue?.Encoding;

HttpContext.Request.Form se convierte en:

if (httpContext.Request.HasFormContentType)
{
IFormCollection form;

form = httpContext.Request.Form; // sync


// Or
form = await httpContext.Request.ReadFormAsync(); // async

string firstName = form["firstname"];


string lastName = form["lastname"];
}

WARNING
Leer valores de formulario sólo si el tipo de contenido sub es x--www-form-urlencoded o datos del formulario.

HttpContext.Request.InputStream se convierte en:

string inputBody;
using (var reader = new System.IO.StreamReader(
httpContext.Request.Body, System.Text.Encoding.UTF8))
{
inputBody = reader.ReadToEnd();
}

WARNING
Use este código solo en un middleware de tipo de controlador, al final de una canalización.
Puede leer el cuerpo sin formato, como se indicó anteriormente una sola vez por solicitud. Middleware de intentar leer el
cuerpo después de la primera lectura leerá un cuerpo vacío.
Esto no se aplica a un formulario de lectura como se muestra anteriormente, ya proviene de un búfer.

HttpContext.Response
HttpContext.Response.Status y HttpContext.Response.StatusDescription traducir al:
// using Microsoft.AspNetCore.Http;
httpContext.Response.StatusCode = StatusCodes.Status200OK;

HttpContext.Response.ContentEncoding y HttpContext.Response.ContentType traducir al:

// using Microsoft.Net.Http.Headers;
var mediaType = new MediaTypeHeaderValue("application/json");
mediaType.Encoding = System.Text.Encoding.UTF8;
httpContext.Response.ContentType = mediaType.ToString();

HttpContext.Response.ContentType en su propio también se convierte en:

httpContext.Response.ContentType = "text/html";

HttpContext.Response.Output se convierte en:

string responseContent = GetResponseContent();


await httpContext.Response.WriteAsync(responseContent);

HttpContext.Response.TransmitFile
Servir el archivo se analiza aquí.
HttpContext.Response.Headers
Enviar encabezados de respuesta se ve complicado por el hecho de que si se establece después de que algo se ha
escrito en el cuerpo de respuesta, no se enviará.
La solución consiste en establecer un método de devolución de llamada que se llamará derecha antes de escribir
en la respuesta se inicia. Es mejor hacerlo al principio de la Invoke método en el middleware. Es este método de
devolución de llamada que establece los encabezados de respuesta.
El código siguiente define un método de devolución de llamada llama SetHeaders :

public async Task Invoke(HttpContext httpContext)


{
// ...
httpContext.Response.OnStarting(SetHeaders, state: httpContext);

El SetHeaders método de devolución de llamada sería similar al siguiente:


// using Microsoft.AspNet.Http.Headers;
// using Microsoft.Net.Http.Headers;

private Task SetHeaders(object context)


{
var httpContext = (HttpContext)context;

// Set header with single value


httpContext.Response.Headers["ResponseHeaderName"] = "headerValue";

// Set header with multiple values


string[] responseHeaderValues = new string[] { "headerValue1", "headerValue1" };
httpContext.Response.Headers["ResponseHeaderName"] = responseHeaderValues;

// Translating ASP.NET 4's HttpContext.Response.RedirectLocation


httpContext.Response.Headers[HeaderNames.Location] = "http://www.example.com";
// Or
httpContext.Response.Redirect("http://www.example.com");

// GetTypedHeaders extension method provides strongly typed access to many headers


var responseHeaders = httpContext.Response.GetTypedHeaders();

// Translating ASP.NET 4's HttpContext.Response.CacheControl


responseHeaders.CacheControl = new CacheControlHeaderValue
{
MaxAge = new System.TimeSpan(365, 0, 0, 0)
// Many more properties available
};

// If you use .Net 4.6+, Task.CompletedTask will be a bit faster


return Task.FromResult(0);
}

HttpContext.Response.Cookies
Las cookies de viaje en el explorador en un Set-Cookie encabezado de respuesta. Como resultado, al enviar
cookies, requiere la misma devolución de llamada que se usan para enviar los encabezados de respuesta:

public async Task Invoke(HttpContext httpContext)


{
// ...
httpContext.Response.OnStarting(SetCookies, state: httpContext);
httpContext.Response.OnStarting(SetHeaders, state: httpContext);

El SetCookies método de devolución de llamada sería similar al siguiente:

private Task SetCookies(object context)


{
var httpContext = (HttpContext)context;

IResponseCookies responseCookies = httpContext.Response.Cookies;

responseCookies.Append("cookie1name", "cookie1value");
responseCookies.Append("cookie2name", "cookie2value",
new CookieOptions { Expires = System.DateTime.Now.AddDays(5), HttpOnly = true });

// If you use .Net 4.6+, Task.CompletedTask will be a bit faster


return Task.FromResult(0);
}

Recursos adicionales
Información general de los módulos HTTP y controladores HTTP
Configuración
Inicio de aplicaciones
Middleware
Migración de ASP.NET Core 1.x a 2.0
21/06/2018 • 13 minutes to read • Edit Online

Por Scott Addie


En este artículo se le guía a lo largo del proceso de actualización de un proyecto existente de ASP.NET Core 1.x a
ASP.NET Core 2.0. La migración de la aplicación a ASP.NET Core 2.0 permite aprovechar muchas características
nuevas y mejoras de rendimiento.
Las aplicaciones existentes de ASP.NET Core 1.x se basan en plantillas de proyecto específicas de la versión. A
medida que el marco de trabajo de ASP.NET Core evoluciona, también lo hacen las plantillas de proyecto y el
código de inicio incluido en ellas. Además de actualizar el marco de trabajo de ASP.NET Core, debe actualizar el
código de la aplicación.

Requisitos previos
Consulte Introducción a ASP.NET Core.

Actualización del moniker de la plataforma de destino (TFM)


Los proyectos para .NET Core deben usar el TFM de una versión mayor o igual que .NET Core 2.0. Busque el
nodo <TargetFramework> del archivo .csproj y reemplace su texto interno por netcoreapp2.0 :

<TargetFramework>netcoreapp2.0</TargetFramework>

Los proyectos para .NET Framework deben usar el TFM de una versión mayor o igual que .NET Framework 4.6.1.
Busque el nodo <TargetFramework> del archivo .csproj y reemplace su texto interno por net461 :

<TargetFramework>net461</TargetFramework>

NOTE
.NET Core 2.0 ofrece un área expuesta mucho mayor que .NET Core 1.x. Si el destino es .NET Framework solo porque faltan
API en .NET Core 1.x, el uso de .NET Core 2.0 como destino es probable que dé resultado.

Actualización de la versión del SDK de .NET Core en global.json


Si la solución se basa en un archivo global.json para que el destino sea una versión específica del SDK de .NET
Core, actualice su propiedad version para usar la versión 2.0 instalada en el equipo:

{
"sdk": {
"version": "2.0.0"
}
}

Actualización de las referencias del paquete


El archivo .csproj de un proyecto de 1.x enumera cada paquete NuGet usado por el proyecto.
En un proyecto de ASP.NET Core 2.0 para .NET Core 2.0, una sola referencia de metapaquete del archivo .csproj
reemplaza a la colección de paquetes:

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" />
</ItemGroup>

Todas las características de ASP.NET Core 2.0 y Entity Framework Core 2.0 están incluidas en el metapaquete.
Los proyectos de ASP.NET Core 2.0 para .NET Framework deben seguir haciendo referencia a paquetes NuGet
individuales. Actualización del atributo Version de cada nodo <PackageReference /> a 2.0.0.
Por ejemplo, esta es la lista de nodos <PackageReference /> usados en un proyecto típico de ASP.NET Core 2.0
para .NET Framework:

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Cookies" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.ViewCompilation" Version="2.0.0"
PrivateAssets="All" />
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="2.0.0" PrivateAssets="All" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="2.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.0.0" PrivateAssets="All" />
<PackageReference Include="Microsoft.VisualStudio.Web.BrowserLink" Version="2.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.0.0"
PrivateAssets="All" />
</ItemGroup>

Actualización de las herramientas de la CLI de .NET Core


En el archivo .csproj, actualice el atributo Version de cada nodo <DotNetCliToolReference /> a 2.0.0.
Por ejemplo, esta es la lista de herramientas de la CLI usadas en un proyecto típico de ASP.NET Core 2.0 para
.NET Core 2.0:

<ItemGroup>
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.0" />
<DotNetCliToolReference Include="Microsoft.Extensions.SecretManager.Tools" Version="2.0.0" />
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.0" />
</ItemGroup>

Cambio de nombre de la propiedad Package Target Fallback


El archivo .csproj de un proyecto de 1.x usa un nodo PackageTargetFallback y una variable:

<PackageTargetFallback>$(PackageTargetFallback);portable-net45+win8+wp8+wpa81;</PackageTargetFallback>

Cambie el nombre del nodo y la variable a AssetTargetFallback :


<AssetTargetFallback>$(AssetTargetFallback);portable-net45+win8+wp8+wpa81;</AssetTargetFallback>

Actualización del método Main de Program.cs


En los proyectos de 1.x, el método Main de Program.cs tenía este aspecto:

using System.IO;
using Microsoft.AspNetCore.Hosting;

namespace AspNetCoreDotNetCore1App
{
public class Program
{
public static void Main(string[] args)
{
var host = new WebHostBuilder()
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<Startup>()
.UseApplicationInsights()
.Build();

host.Run();
}
}
}

En los proyectos de 2.0, el método Main de Program.cs se ha simplificado:

using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;

namespace AspNetCoreDotNetCore2App
{
public class Program
{
public static void Main(string[] args)
{
BuildWebHost(args).Run();
}

public static IWebHost BuildWebHost(string[] args) =>


WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.Build();
}
}

La adopción de este nuevo patrón 2.0 es muy recomendable y necesaria para que funcionen características de
producto como las migraciones de Entity Framework (EF ) Core. Por ejemplo, la ejecución de Update-Database
desde la ventana Consola del Administrador de paquetes o de dotnet ef database update desde la línea de
comandos (en proyectos convertidos a ASP.NET Core 2.0) genera el error siguiente:

Unable to create an object of type '<Context>'. Add an implementation of


'IDesignTimeDbContextFactory<Context>' to the project, or see https://go.microsoft.com/fwlink/?linkid=851728
for additional patterns supported at design time.
Incorporación de proveedores de configuración
En los proyectos de 1.x, la incorporación de proveedores de configuración a una aplicación se lograba a través del
constructor Startup . Para ello, era necesario crear una instancia de ConfigurationBuilder , cargar los proveedores
aplicables (variables de entorno, configuración de la aplicación, etc.) e inicializar un miembro de
IConfigurationRoot .

public Startup(IHostingEnvironment env)


{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);

if (env.IsDevelopment())
{
builder.AddUserSecrets<Startup>();
}

builder.AddEnvironmentVariables();
Configuration = builder.Build();
}

public IConfigurationRoot Configuration { get; }

El ejemplo anterior carga el miembro Configuration con opciones de configuración de appsettings.json, así como
cualquier archivo appsettings.<EnvironmentName>.json que coincida con la propiedad
IHostingEnvironment.EnvironmentName . La ubicación de estos archivos está en la misma ruta de acceso que
Startup.cs.
En los proyectos de 2.0, el código de configuración reutilizable inherente a los proyectos de 1.x se ejecuta en
segundo plano. Por ejemplo, las variables de entorno y la configuración de la aplicación se cargan durante el inicio.
El código de Startup.cs equivalente se reduce a la inicialización de IConfiguration con la instancia insertada:

public Startup(IConfiguration configuration)


{
Configuration = configuration;
}

public IConfiguration Configuration { get; }

Para quitar los proveedores predeterminados que WebHostBuilder.CreateDefaultBuilder agrega, invoque el


método Clear en la propiedad IConfigurationBuilder.Sources dentro de ConfigureAppConfiguration . Para volver
a agregar proveedores, use el método ConfigureAppConfiguration en Program.cs:
public static void Main(string[] args)
{
BuildWebHost(args).Run();
}

public static IWebHost BuildWebHost(string[] args) =>


WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.ConfigureAppConfiguration((hostContext, config) =>
{
// delete all default configuration providers
config.Sources.Clear();
config.AddJsonFile("myconfig.json", optional: true);
})
.Build();

La configuración que el método CreateDefaultBuilder utiliza en el fragmento de código anterior puede verse aquí.
Para más información, consulte Configuración en ASP.NET Core.

Mover el código de inicialización de la base de datos


En proyectos de 1.x que usen EF Core 1.x, un comando como dotnet ef migrations add hace lo siguiente:
1. Crea una instancia de Startup .
2. Invoca el método ConfigureServices para registrar todos los servicios de la inserción de dependencias (como
los tipos DbContext ).
3. Realiza las tareas necesarias.
En proyectos 2.0 que usen EF Core 2.0, se invoca Program.BuildWebHost para obtener los servicios de aplicación. A
diferencia de 1.x, se invoca Startup.Configure como efecto secundario adicional. Si la aplicación 1.x ha invocado el
código de inicialización de la aplicación en el método Configure , pueden producirse errores inesperados. Por
ejemplo, si todavía no existe la base de datos, el código de propagación se ejecuta antes que el comando EF Core
Migrations. Este problema provocará un error en un comando dotnet ef migrations list si la base de datos no
existe.
Tenga en cuenta el código de propagación 1.x siguiente en el método Configure de Startup.cs:

app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});

SeedData.Initialize(app.ApplicationServices);

En los proyectos 2.0, mueva la llamada SeedData.Initialize al método Main de Program.cs:


var host = BuildWebHost(args);

using (var scope = host.Services.CreateScope())


{
var services = scope.ServiceProvider;

try
{
// Requires using RazorPagesMovie.Models;
SeedData.Initialize(services);
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred seeding the DB.");
}
}

host.Run();

A partir de 2.0 se desaconseja cualquier acción en BuildWebHost , excepto la compilación y la configuración del
host web. Todo lo relacionado con la ejecución de la aplicación deberá gestionarse fuera de BuildWebHost —,
normalmente en el método Main de Program.cs.

Revisión de la configuración de compilación de la vista Razor


Un tiempo de inicio de aplicación más rápido y unos lotes publicados más pequeños son de la máxima
importancia para el usuario. Por ello, la compilación de la vista Razor está habilitada de forma predeterminada en
ASP.NET Core 2.0.
Ya no es necesario establecer la propiedad MvcRazorCompileOnPublish en true. A menos que se esté deshabilitando
la compilación de la vista, se puede quitar la propiedad del archivo .csproj.
Cuando el destino es .NET Framework, se debe hacer referencia de forma explícita al paquete NuGet
Microsoft.AspNetCore.Mvc.Razor.ViewCompilation en el archivo .csproj:

<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.ViewCompilation" Version="2.0.0" PrivateAssets="All"


/>

Características "light-up" de Application Insights como base


Es importante configurar sin esfuerzo la instrumentación de rendimiento de la aplicación. Ahora puede basarse en
las nuevas características "light-up" de Application Insights disponibles en las herramientas de Visual Studio 2017.
Los proyectos de ASP.NET Core 1.1 creados en Visual Studio 2017 han agregado Application Insights de forma
predeterminada. Si no usa el SDK de Application Insights directamente, fuera de Program.cs y Startup.cs, siga
estos pasos:
1. Si el destino es .NET Core, elimine el siguiente nodo <PackageReference /> del archivo .csproj:

<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.0.0" />

2. Si el destino es .NET Core, elimine la invocación del método de extensión UseApplicationInsights de


Program.cs:
public static void Main(string[] args)
{
var host = new WebHostBuilder()
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<Startup>()
.UseApplicationInsights()
.Build();

host.Run();
}

3. Quite la llamada API del lado cliente de Application Insights de _Layout.cshtml. Comprende las dos líneas
de código siguientes:

@inject Microsoft.ApplicationInsights.AspNetCore.JavaScriptSnippet JavaScriptSnippet


@Html.Raw(JavaScriptSnippet.FullScript)

Si usa el SDK de Application Insights directamente, siga haciéndolo. El metapaquete 2.0 incluye la versión más
reciente de Application Insights, por lo que si se hace referencia a una versión anterior, aparece un error de
degradación de paquete.

Adopción de mejoras de autenticación o identidad


ASP.NET Core 2.0 tiene un nuevo modelo de autenticación y una serie de cambios significativos en ASP.NET Core
Identity. Si ha creado el proyecto con la opción Cuentas de usuario individuales habilitada o si ha agregado
manualmente la autenticación o Identity, vea Migración de la autenticación e Identity a ASP.NET Core 2.0.

Recursos adicionales
Cambios importantes en ASP.NET Core 2.0
Migrar de autenticación e identidad a ASP.NET Core
2.0
22/06/2018 • 14 minutes to read • Edit Online

Por Scott Addie y Hao Kung


Núcleo de ASP.NET 2.0 tiene un nuevo modelo para la autenticación y identidad que simplifica la configuración
mediante el uso de servicios. Las aplicaciones de ASP.NET Core 1.x que utilice la autenticación o la identidad se
pueden actualizar para usar el nuevo modelo tal como se describe a continuación.

Middleware de autenticación y servicios


En los proyectos de 1.x, se configura la autenticación a través de middleware. Se invoca un método de middleware
para cada esquema de autenticación que desea admitir.
El siguiente ejemplo de 1.x configura la autenticación de Facebook con identidad en Startup.cs:

public void ConfigureServices(IServiceCollection services)


{
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
}

public void Configure(IApplicationBuilder app, ILoggerFactory loggerfactory)


{
app.UseIdentity();
app.UseFacebookAuthentication(new FacebookOptions {
AppId = Configuration["auth:facebook:appid"],
AppSecret = Configuration["auth:facebook:appsecret"]
});
}

En los 2.0 proyectos, se configura la autenticación a través de servicios. Cada esquema de autenticación está
registrado en el ConfigureServices método Startup.cs. El UseIdentity método se sustituye por
UseAuthentication .

El siguiente ejemplo 2.0 configura la autenticación de Facebook con identidad en Startup.cs:


public void ConfigureServices(IServiceCollection services)
{
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();

// If you want to tweak Identity cookies, they're no longer part of IdentityOptions.


services.ConfigureApplicationCookie(options => options.LoginPath = "/Account/LogIn");
services.AddAuthentication()
.AddFacebook(options =>
{
options.AppId = Configuration["auth:facebook:appid"];
options.AppSecret = Configuration["auth:facebook:appsecret"];
});
}

public void Configure(IApplicationBuilder app, ILoggerFactory loggerfactory) {


app.UseAuthentication();
}

El UseAuthentication método agrega un componente de middleware de autenticación único que es responsable


de la autenticación automática y el control de solicitudes de autenticación remota. Reemplaza todos los
componentes de middleware individuales con un componente de middleware única y común.
A continuación se muestran 2.0 instrucciones de migración para cada esquema de autenticación principales.
Autenticación basada en cookies
Seleccione una de las dos opciones siguientes y realice los cambios necesarios en Startup.cs:
1. Usar cookies con identidad
Reemplace UseIdentity con UseAuthentication en el Configure método:

app.UseAuthentication();

Invocar la AddIdentity método en el ConfigureServices método para agregar los servicios de


autenticación de la cookie.
Si lo desea, invocar el ConfigureApplicationCookie o ConfigureExternalCookie método en el
ConfigureServices método retocar la configuración de cookies de identidad.

services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();

services.ConfigureApplicationCookie(options => options.LoginPath = "/Account/LogIn");

2. Usar cookies sin identidad


Reemplace el UseCookieAuthentication llamada al método el Configure método con
UseAuthentication :

app.UseAuthentication();

Invocar la AddAuthentication y AddCookie métodos en el ConfigureServices método:


// If you don't want the cookie to be automatically authenticated and assigned to
HttpContext.User,
// remove the CookieAuthenticationDefaults.AuthenticationScheme parameter passed to
AddAuthentication.
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = "/Account/LogIn";
options.LogoutPath = "/Account/LogOff";
});

Autenticación de portador JWT


Realice los cambios siguientes en Startup.cs:
Reemplace el UseJwtBearerAuthentication llamada al método el Configure método con UseAuthentication :

app.UseAuthentication();

Invocar la AddJwtBearer método en el ConfigureServices método:

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Audience = "http://localhost:5001/";
options.Authority = "http://localhost:5000/";
});

Este fragmento de código no utiliza la identidad, por lo que el esquema predeterminado debe establecerse
pasando JwtBearerDefaults.AuthenticationScheme a la AddAuthentication método.
Autenticación de OpenID conectarse (OIDC )
Realice los cambios siguientes en Startup.cs:
Reemplace el UseOpenIdConnectAuthentication llamada al método el Configure método con
UseAuthentication :

app.UseAuthentication();

Invocar la AddOpenIdConnect método en el ConfigureServices método:

services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
options.Authority = Configuration["auth:oidc:authority"];
options.ClientId = Configuration["auth:oidc:clientid"];
});

autenticación de Facebook
Realice los cambios siguientes en Startup.cs:
Reemplace el UseFacebookAuthentication llamada al método el Configure método con UseAuthentication :
app.UseAuthentication();

Invocar la AddFacebook método en el ConfigureServices método:

services.AddAuthentication()
.AddFacebook(options =>
{
options.AppId = Configuration["auth:facebook:appid"];
options.AppSecret = Configuration["auth:facebook:appsecret"];
});

Autenticación de Google
Realice los cambios siguientes en Startup.cs:
Reemplace el UseGoogleAuthentication llamada al método el Configure método con UseAuthentication :

app.UseAuthentication();

Invocar la AddGoogle método en el ConfigureServices método:

services.AddAuthentication()
.AddGoogle(options =>
{
options.ClientId = Configuration["auth:google:clientid"];
options.ClientSecret = Configuration["auth:google:clientsecret"];
});

Autenticación de Microsoft Account


Realice los cambios siguientes en Startup.cs:
Reemplace el UseMicrosoftAccountAuthentication llamada al método el Configure método con
UseAuthentication :

app.UseAuthentication();

Invocar la AddMicrosoftAccount método en el ConfigureServices método:

services.AddAuthentication()
.AddMicrosoftAccount(options =>
{
options.ClientId = Configuration["auth:microsoft:clientid"];
options.ClientSecret = Configuration["auth:microsoft:clientsecret"];
});

Autenticación de Twitter
Realice los cambios siguientes en Startup.cs:
Reemplace el UseTwitterAuthentication llamada al método el Configure método con UseAuthentication :

app.UseAuthentication();

Invocar la AddTwitter método en el ConfigureServices método:


services.AddAuthentication()
.AddTwitter(options =>
{
options.ConsumerKey = Configuration["auth:twitter:consumerkey"];
options.ConsumerSecret = Configuration["auth:twitter:consumersecret"];
});

Esquemas de autenticación predeterminado de configuración


En 1.x, la AutomaticAuthenticate y AutomaticChallenge propiedades de la AuthenticationOptions clase base se
pensada para establecerse en un esquema de autenticación único. No había forma de buena para exigir esto.
En 2.0, se han quitado estas dos propiedades como propiedades en la persona AuthenticationOptions instancia.
Se pueden configurar en el AddAuthentication llamada al método dentro de la ConfigureServices método
Startup.cs:

services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme);

En el fragmento de código anterior, el esquema predeterminado se establece en


CookieAuthenticationDefaults.AuthenticationScheme ("Cookies").

Como alternativa, use una versión sobrecargada de la AddAuthentication método para establecer más de una
propiedad. En el siguiente ejemplo de método sobrecargado, el esquema predeterminado se establece en
CookieAuthenticationDefaults.AuthenticationScheme . O bien se puede especificar el esquema de autenticación
dentro de la persona [Authorize] atributos o las directivas de autorización.

services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
});

Definir un esquema predeterminado en 2.0 si se cumple alguna de las condiciones siguientes:


Desea que el usuario para iniciar sesión automáticamente en
Usa el [Authorize] las directivas de autorización o de atributo sin especificar esquemas
Una excepción a esta regla es la AddIdentity método. Este método agrega cookies a usted y establece el valor
predeterminado autenticarse y desafío esquemas a la cookie de aplicación IdentityConstants.ApplicationScheme .
Además, Establece el esquema predeterminado de inicio de sesión en la cookie externa
IdentityConstants.ExternalScheme .

Usar las extensiones de autenticación de HttpContext


La IAuthenticationManager interfaz es el punto de entrada principal en el sistema de autenticación 1.x. Se ha
reemplazado por un nuevo conjunto de HttpContext métodos de extensión en la
Microsoft.AspNetCore.Authentication espacio de nombres.

Por ejemplo, 1.x proyectos referencia un Authentication propiedad:

// Clear the existing external cookie to ensure a clean login process


await HttpContext.Authentication.SignOutAsync(_externalCookieScheme);

En los 2.0 proyectos, importar la Microsoft.AspNetCore.Authentication espacio de nombres y eliminar el


Authentication referencias de propiedad:

// Clear the existing external cookie to ensure a clean login process


await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);

Autenticación de Windows (HTTP.sys / IISIntegration)


Hay dos variaciones de autenticación de Windows:
1. El host sólo permite que los usuarios autenticados
2. El host permite que ambas anónimo y los usuarios autenticados
No se ve afectada por los cambios de 2.0 la primera variación que se ha descrito anteriormente.
La segunda variación que se ha descrito anteriormente se ve afectada por los 2.0 cambios. Por ejemplo, puede
permitir a los usuarios anónimos en su aplicación en IIS o HTTP.sys pero autorizando a los usuarios en el nivel de
controlador de las capas. En este escenario, establezca el esquema predeterminado
IISDefaults.AuthenticationScheme en el ConfigureServices método Startup.cs:

services.AddAuthentication(IISDefaults.AuthenticationScheme);

Para establecer el esquema predeterminado en consecuencia, se impiden la solicitud de autorización para su


realización del trabajo.

Instancias de IdentityCookieOptions
Un efecto secundario de los 2.0 cambios es el conmutador que se va a usar con el nombre de opciones en lugar de
instancias de opciones de cookie. Se quita la capacidad de personalizar los nombres de esquema de cookie de
identidad.
Por ejemplo, los proyectos utilizan 1.x inyección de constructor para pasar un IdentityCookieOptions parámetros
en AccountController.cs. El esquema de autenticación de cookie externo se tiene acceso desde la instancia
proporcionada:

public AccountController(
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager,
IOptions<IdentityCookieOptions> identityCookieOptions,
IEmailSender emailSender,
ISmsSender smsSender,
ILoggerFactory loggerFactory)
{
_userManager = userManager;
_signInManager = signInManager;
_externalCookieScheme = identityCookieOptions.Value.ExternalCookieAuthenticationScheme;
_emailSender = emailSender;
_smsSender = smsSender;
_logger = loggerFactory.CreateLogger<AccountController>();
}

La inyección de constructor mencionado anteriormente, se convierte en innecesaria en los proyectos de 2.0 y el


_externalCookieScheme campo se puede eliminar:
public AccountController(
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager,
IEmailSender emailSender,
ISmsSender smsSender,
ILoggerFactory loggerFactory)
{
_userManager = userManager;
_signInManager = signInManager;
_emailSender = emailSender;
_smsSender = smsSender;
_logger = loggerFactory.CreateLogger<AccountController>();
}

El IdentityConstants.ExternalScheme constante se puede usar directamente:

// Clear the existing external cookie to ensure a clean login process


await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);

Agregar propiedades de navegación IdentityUser POCO


Las propiedades de navegación de núcleo de Entity Framework (EF ) de la base de IdentityUser POCO (objeto
CLR antiguos sin formato) se han quitado. Si el proyecto 1.x utiliza estas propiedades, agregarlos manualmente al
proyecto 2.0:

/// <summary>
/// Navigation property for the roles this user belongs to.
/// </summary>
public virtual ICollection<IdentityUserRole<int>> Roles { get; } = new List<IdentityUserRole<int>>();

/// <summary>
/// Navigation property for the claims this user possesses.
/// </summary>
public virtual ICollection<IdentityUserClaim<int>> Claims { get; } = new List<IdentityUserClaim<int>>();

/// <summary>
/// Navigation property for this users login accounts.
/// </summary>
public virtual ICollection<IdentityUserLogin<int>> Logins { get; } = new List<IdentityUserLogin<int>>();

Para evitar que las claves externas duplicadas al ejecutar migraciones de núcleo de EF, agregue lo siguiente a su
IdentityDbContext clase OnModelCreating método (después de la base.OnModelCreating(); llamar a):
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
// Customize the ASP.NET Identity model and override the defaults if needed.
// For example, you can rename the ASP.NET Identity table names and more.
// Add your customizations after calling base.OnModelCreating(builder);

builder.Entity<ApplicationUser>()
.HasMany(e => e.Claims)
.WithOne()
.HasForeignKey(e => e.UserId)
.IsRequired()
.OnDelete(DeleteBehavior.Cascade);

builder.Entity<ApplicationUser>()
.HasMany(e => e.Logins)
.WithOne()
.HasForeignKey(e => e.UserId)
.IsRequired()
.OnDelete(DeleteBehavior.Cascade);

builder.Entity<ApplicationUser>()
.HasMany(e => e.Roles)
.WithOne()
.HasForeignKey(e => e.UserId)
.IsRequired()
.OnDelete(DeleteBehavior.Cascade);
}

Reemplazar GetExternalAuthenticationSchemes
El método sincrónico GetExternalAuthenticationSchemes se quitó en favor de una versión asincrónica. 1.x
proyectos tienen el siguiente código ManageController.cs:

var otherLogins = _signInManager.GetExternalAuthenticationSchemes().Where(auth => userLogins.All(ul =>


auth.AuthenticationScheme != ul.LoginProvider)).ToList();

Este método aparece en Login.cshtml demasiado:

var loginProviders = SignInManager.GetExternalAuthenticationSchemes().ToList();


<div>
<p>
@foreach (var provider in loginProviders)
{
<button type="submit" class="btn btn-default" name="provider"
value="@provider.AuthenticationScheme" title="Log in using your @provider.DisplayName
account">@provider.AuthenticationScheme</button>
}
</p>
</div>
</form>
}

En los 2.0 proyectos, utilice la GetExternalAuthenticationSchemesAsync método:

var schemes = await _signInManager.GetExternalAuthenticationSchemesAsync();


var otherLogins = schemes.Where(auth => userLogins.All(ul => auth.Name != ul.LoginProvider)).ToList();

En Login.cshtml, AuthenticationScheme propiedad accede en la foreach bucle cambia a Name :


var loginProviders = (await SignInManager.GetExternalAuthenticationSchemesAsync()).ToList();
<div>
<p>
@foreach (var provider in loginProviders)
{
<button type="submit" class="btn btn-default" name="provider" value="@provider.Name"
title="Log in using your @provider.DisplayName account">@provider.DisplayName</button>
}
</p>
</div>
</form>
}

Cambio de propiedad ManageLoginsViewModel


A ManageLoginsViewModel objeto se usa en la ManageLogins acción de ManageController.cs. En 1.x proyectos, el
objeto OtherLogins propiedad de valor devuelto es de tipo IList<AuthenticationDescription> . Este tipo de valor
devuelto requiere una importación de Microsoft.AspNetCore.Http.Authentication :

using System.Collections.Generic;
using Microsoft.AspNetCore.Http.Authentication;
using Microsoft.AspNetCore.Identity;

namespace AspNetCoreDotNetCore1App.Models.ManageViewModels
{
public class ManageLoginsViewModel
{
public IList<UserLoginInfo> CurrentLogins { get; set; }

public IList<AuthenticationDescription> OtherLogins { get; set; }


}
}

En los 2.0 proyectos, se cambia el tipo de valor devuelto a IList<AuthenticationScheme> . Este nuevo tipo de valor
devuelto es necesario sustituir la Microsoft.AspNetCore.Http.Authentication importar con un
Microsoft.AspNetCore.Authentication importar.

using System.Collections.Generic;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;

namespace AspNetCoreDotNetCore2App.Models.ManageViewModels
{
public class ManageLoginsViewModel
{
public IList<UserLoginInfo> CurrentLogins { get; set; }

public IList<AuthenticationScheme> OtherLogins { get; set; }


}
}

Recursos adicionales
Para obtener más detalles y explicación, consulte el discusión para autenticación 2.0 problema en GitHub.

También podría gustarte