Вы находитесь на странице: 1из 2758

Introducción a ASP.

NET Core
29/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.
Novedades de ASP.NET Core 2.1
31/08/2018 • 13 minutes to read • Edit Online

En este artículo se resaltan los cambios más importantes de ASP.NET Core 2.1, con vínculos a la documentación
pertinente.

SignalR
SignalR se ha reescrito para ASP.NET Core 2.1. SignalR de ASP.NET Core incluye una serie de mejoras:
Un modelo de escalabilidad horizontal simplificado.
Un nuevo cliente de JavaScript sin dependencias de jQuery.
Un nuevo protocolo binario compacto basado en MessagePack.
Compatibilidad con protocolos personalizados.
Un nuevo modelo de respuesta de streaming.
Compatibilidad con clientes basados en WebSockets vacíos.
Para más información, vea SignalR de ASP.NET Core.

Bibliotecas de clases de Razor


Con ASP.NET Core 2.1 es más fácil crear e incluir una interfaz de usuario basada en Razor en una biblioteca y
compartirla entre varios proyectos. El nuevo SDK de Razor permite crear archivos de Razor en un proyecto de
biblioteca de clases que se puede empaquetar en un paquete NuGet. Las vistas y las páginas en las bibliotecas se
detectan automáticamente y se pueden reemplazar por la aplicación. Al integrar la compilación de Razor en la
versión de compilación:
El tiempo de inicio de la aplicación es mucho más rápido.
Sigue habiendo disponibles actualizaciones rápidas de las páginas y vistas de Razor en tiempo de ejecución
como parte de un flujo de trabajo de desarrollo iterativo.
Para más información, vea Create reusable UI using the Razor Class Library project (Crear una interfaz de usuario
reutilizable con el proyecto de biblioteca de clases de Razor).

Aplicación de scaffolding y biblioteca de interfaz de usuario de


identidad
ASP.NET Core 2.1 proporciona ASP.NET Core Identity como un biblioteca de clases de Razor. Las aplicaciones
que incluyan Identity pueden aplicar el nuevo proveedor de scaffolding de Identity para agregar de forma selectiva
el código fuente contenido en la biblioteca de clases de Razor (RCL ) de Identidad. Puede que quiera generar
código fuente que le permita modificar un código y cambiar el comportamiento; así, por ejemplo, podría indicar al
proveedor de scaffolding que generara el código que se usa en el registro. Dicho código generado tendrá prioridad
sobre el mismo código en el RCL de Identity.
Las aplicaciones que no incluyan autenticación puede aplicar el proveedor de scaffolding de Identity para agregar
el paquete de RCL de Identity. Existe la posibilidad de seleccionar el código de Identity que se va a generar.
Para más información, vea Scaffold Identity in ASP.NET Core projects (Identidad de scaffold en proyectos de
ASP.NET Core).
HTTPS
En un momento en que la seguridad y la privacidad tienen cada vez más relevancia, es importante habilitar HTTPS
en las aplicaciones web. El cumplimiento de HTTPS es cada vez más estricto en Internet. Los sitios que no usan
HTTPS se consideran inseguros. Los exploradores (Chrome, Mozilla) están empezando a exigir el uso de las
características web dentro de un contexto seguro. El RGPD exige el uso de HTTPS para proteger la privacidad de
los usuarios. Usar HTTPS en la fase de producción es esencial, pero hacerlo también en la fase de desarrollo puede
ayudar a evitar problemas en la implementación (por ejemplo, vínculos inseguros). ASP.NET Core 2.1 incluye
diversas mejoras que hacen que sea más fácil usar HTTPS en el desarrollo y configurar el protocolo HTTPS en la
producción. Para más información, vea Aplicación de HTTPS.
Activado de forma predeterminada
A fin de hacer posible un desarrollo de sitios web seguro, ahora HTTPS está habilitado de forma predeterminada.
A partir de 2.1, Kestrel escucha en https://localhost:5001 cuando hay presente un certificado de desarrollo local.
Un certificado de desarrollo se crea:
Como parte de la experiencia de primera ejecución del SDK de .NET Core, cuando el SDK se usa por primera
vez.
Manualmente, por medio de la nueva herramienta dev-certs .
Ejecute dotnet dev-certs https --trust para confiar en el certificado.
Cumplimiento y redireccionamiento de HTTPS
Normalmente, las aplicaciones web necesitan escuchar en HTTP y HTTPS, si bien luego redirigen todo el tráfico
HTTP a HTTPS. En 2.1 se ha incluido un middleware especializado de redireccionamiento de HTTPS que redirige
de forma inteligente según la presencia de puertos de servidor enlazado o configuración.
El uso de HTTPS puede exigir aún más por medio del protocolo de Seguridad de transporte estricta de HTTP
(HSTS ). HSTS indica a los exploradores que tengan acceso al sitio siempre a través de HTTPS. ASP.NET Core 2.1
agrega middleware de HSTS que contempla opciones de antigüedad máxima, subdominios y la lista de carga
previa de HSTS.
Configuración para producción
En un entorno de producción, HTTPS se debe configurar explícitamente. En 2.1, se ha agregado un esquema de
configuración predeterminado para configurar HTTPS para Kestrel. Las aplicaciones se pueden configurar para
usar:
Varios puntos de conexión (direcciones URL incluidas). Para más información, vea Implementación del servidor
web Kestrel: Configuración de punto de conexión.
El certificado que se va a usar para HTTPS desde un archivo en disco o desde un almacén de certificados.

RGPD
ASP.NET Core proporciona API y plantillas para cumplir algunos de los requisitos del Reglamento general de
protección de datos (RGPD ) de la UE. Para más información, vea GDPR support in ASP.NET Core (Compatibilidad
con el Reglamento general de protección de datos en ASP.NET Core). Con las aplicaciones de muestra se muestra
cómo usar y probar la mayor parte de las API y los puntos de extensión del RGPD que se han agregado a las
plantillas de ASP.NET Core 2.1.

Pruebas de integración
Se ha incorporado un nuevo paquete que optimiza las tareas de creación y ejecución de pruebas. El paquete
Microsoft.AspNetCore.Mvc.Testing se encarga de estas tareas:
Copia el archivo de dependencia (*.deps) de la aplicación que se está probando en la carpeta bin del proyecto de
prueba.
Establece la raíz de contenido en la raíz de proyecto de la aplicación que se está probando, lo que permite
encontrar archivos estáticos y páginas o vistas cuando se ejecutan las pruebas.
Proporciona la clase WebApplicationFactory para optimizar el arranque de la aplicación que se está probando
con TestServer.
En la siguiente prueba se usa xUnit para comprobar que la página de índice se carga con un código de estado
correcto y con el encabezado Content-Type apropiado:

public class BasicTests


: IClassFixture<WebApplicationFactory<RazorPagesProject.Startup>>
{
private readonly HttpClient _client;

public BasicTests(WebApplicationFactory<RazorPagesProject.Startup> factory)


{
_client = factory.CreateClient();
}

[Fact]
public async Task GetHomePage()
{
// Act
var response = await _client.GetAsync("/");

// Assert
response.EnsureSuccessStatusCode(); // Status Code 200-299
Assert.Equal("text/html; charset=utf-8",
response.Content.Headers.ContentType.ToString());
}
}

Para más información, vea el tema Pruebas de integración.

[ApiController], ActionResult<T>
ASP.NET Core 2.1 presenta nuevas convenciones de programación que hacen que sea más fácil crear y limpiar
API web descriptivas. ActionResult<T> es un nuevo tipo que se ha agregado para que una aplicación pueda
devolver un tipo de respuesta o cualquier otro resultado de acción (similar a IActionResult), sin dejar de indicar el
tipo de respuesta. El atributo [ApiController] se ha agregado también como una forma de decidir si usar
convenciones y comportamientos específicos de las API web.
Para más información, vea Compilación de API web con ASP.NET Core.

IHttpClientFactory
ASP.NET Core 2.1 incluye un nuevo servicio IHttpClientFactory que facilita la configuración y uso de instancias
de HttpClient en las aplicaciones. HttpClient ya posee el concepto de controladores de delegación, que se
pueden vincular entre sí para las solicitudes HTTP salientes. Este servicio:
Hace que el registro de instancias de HttpClient por cliente con nombre sea más intuitivo.
Implementa un controlador de Polly que permite usar directivas de Polly en directivas de reintentos, de
interruptores de circuitos, etc.
Para más información, vea Inicio de solicitudes HTTP.

Configuración de transporte de Kestrel


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. Para más información, vea Implementación del servidor web Kestrel:
Configuración de transporte.

Generador de host genérico


Se ha incluido el generador de host genérico ( HostBuilder ), que se puede usar con aplicaciones que no procesan
solicitudes HTTP (mensajería, tareas en segundo plano, etc.).
Para más información, vea Host genérico de .NET.

Plantillas de SPA actualizadas


Las plantillas de aplicación de página única para Angular, React y React con Redux se han actualizado y ahora usan
sistemas de generación y estructuras de proyecto estándar en cada marco.
La plantilla Angular se basa en la CLI de Angular, mientras que las plantillas de React se basan en create-react-app.
Para más información, vea Uso de las plantillas de aplicación de una sola página con ASP.NET Core.

Búsqueda de activos de Razor en Razor Pages


En la versión 2.1, Razor Pages busca activos de Razor (como diseños y líneas de código parcialmente ejecutadas)
en los siguientes directorios en el orden indicado:
1. Carpeta Current Pages
2. /Pages/Shared/
3. /Views/Shared/

Razor Pages en un área


Razor Pages ya admite las áreas. Para ver un ejemplo de áreas, cree una aplicación web de Razor Pages con
cuentas de usuario individuales. Las aplicaciones web de Razor Pages con cuentas de usuario individuales incluyen
/Areas/Identity/Pages.

Versión de compatibilidad de MVC


El método SetCompatibilityVersion permite a una aplicación participar o no en los cambios de comportamiento
importantes incorporados en ASP.NET Core MVC 2.1 o una versión posterior.
Para obtener más información, vea Versión de compatibilidad para ASP.NET Core MVC.

Migración de 2.0 a 2.1


Vea Migrate from ASP.NET Core 2.0 to 2.1 (Migración de ASP.NET Core 2.0 a 2.1).

Información adicional
Para ver la lista completa de cambios, vea las notas de la versión de ASP.NET Core 2.1.
Novedades de ASP.NET Core 2.0
31/08/2018 • 13 minutes to read • Edit Online

En este artículo se resaltan los cambios más importantes de ASP.NET Core 2.0, con vínculos a la documentación
pertinente.

Páginas de Razor
Las páginas de Razor son una nueva característica de ASP.NET Core MVC que facilita la codificación de escenarios
centrados en páginas y hace que sea más productiva.
Para más información, vea la introducción y el tutorial:
Introducción a las páginas de Razor
Introducción a las páginas de Razor

Metapaquete de ASP.NET Core


Hay un nuevo metapaquete de ASP.NET Core que incluye todos los paquetes creados y que son compatibles con
los equipos de ASP.NET Core y Entity Framework Core, junto con sus dependencias internas y de terceros. Ya no
tiene que elegir características concretas de ASP.NET Core por paquete. Todas las características se incluyen en el
paquete Microsoft.AspNetCore.All. Las plantillas predeterminadas usan este paquete.
Para más información, vea Microsoft.AspNetCore.All metapackage for ASP.NET Core 2.0 (Metapaquete
Microsoft.AspNetCore.All para ASP.NET Core 2.0).

Almacén en tiempo de ejecución


Las aplicaciones que usan el metapaquete Microsoft.AspNetCore.All pueden aprovechar automáticamente el
nuevo almacén en tiempo de ejecución de .NET Core. El almacén contiene todos los recursos en tiempo de
ejecución necesarios para ejecutar aplicaciones de ASP.NET Core 2.0. Al usar el metapaquete
Microsoft.AspNetCore.All , no se implementa ningún recurso de los paquetes NuGet de ASP.NET Core
referenciados con la aplicación, porque ya residen en el sistema de destino. Los recursos del almacén en tiempo de
ejecución también se precompilan para mejorar el tiempo de inicio de la aplicación.
Para más información, vea Runtime store (Almacén en tiempo de ejecución).

.NET Standard 2.0


Los paquetes de ASP.NET Core 2.0 tienen como destino .NET Standard 2.0. Se puede hacer referencia a los
paquetes mediante otras bibliotecas de .NET Standard 2.0 y se pueden ejecutar en implementaciones compatibles
con .NET Standard 2.0 de. NET, como .NET Core 2.0 y .NET Framework 4.6.1.
El metapaquete Microsoft.AspNetCore.All tiene como destino únicamente .NET Core 2.0, ya que está pensado
para usarse con el almacén en tiempo de ejecución de .NET Core 2.0.

Actualización de la configuración
En ASP.NET Core 2.0 se agrega de forma predeterminada una instancia IConfiguration al contenedor de
servicios. La instancia IConfiguration del contenedor de servicios facilita que las aplicaciones recuperen los
valores de configuración del contenedor.
Para información sobre el estado de la documentación planeada, vea este problema de GitHub.

Actualización del registro


En ASP.NET Core 2.0, el registro se incorpora de forma predeterminada en el sistema de inserción de
dependencias (DI). Debe agregar proveedores y configurar el filtrado en el archivo Program.cs, y no en el archivo
Startup.cs. El ILoggerFactory predeterminado admite el filtrado de una forma que le permite usar un enfoque
flexible para el filtrado de varios proveedores y el filtrado de proveedor específico.
Para más información, vea Introduction to Logging (Introducción al registro).

Actualización de la autenticación
Hay un nuevo modelo de autenticación que facilita la configuración de la autenticación de una aplicación mediante
la inserción de dependencias.
Hay plantillas nuevas disponibles para configurar la autenticación de aplicaciones web y API web con [Azure AD
B2C ] (https://azure.microsoft.com/services/active-directory-b2c/).
Para información sobre el estado de la documentación planeada, vea este problema de GitHub.

Actualización de la identidad
Hemos hecho que resulte más fácil crear API web seguras mediante la identidad en ASP.NET Core 2.0. Puede
adquirir tokens de acceso para obtener acceso a las API web mediante la Biblioteca de autenticación de Microsoft
(MSAL ).
Para más información sobre los cambios de autenticación en la versión 2.0, vea los siguientes recursos:
Confirmación de las cuentas y recuperación de contraseñas en ASP.NET Core
Habilitar la generación de códigos QR para las aplicaciones de autenticación en ASP.NET Core
Migrar la autenticación y la identidad a ASP.NET Core 2.0

Plantillas de SPA
Hay disponibles plantillas de proyectos de Single-Page Application (SPA) para Angular, Aurelia, Knockout.js,
React.js y React.js con Redux. La plantilla de Angular se ha actualizado a Angular 4. Las plantillas de Angular y de
React están disponibles de forma predeterminada. Para obtener información sobre cómo obtener las otras
plantillas, vea Creating a new SPA project (Crear un proyecto de SPA). Para más información sobre cómo crear una
SPA en ASP.NET Core, vea Using JavaScriptServices for Creating Single Page Applications (Uso de
JavaScriptServices para crear aplicaciones SPA).

Mejoras en Kestrel
El servidor web de Kestrel tiene nuevas características que lo hacen más adecuado como servidor con conexión a
Internet. Se ha agregado una serie de opciones de configuración de restricción del servidor en la nueva propiedad
Limits de la clase KestrelServerOptions . Agregue límites para:

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.
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).
WebListener pasa a denominarse HTTP.sys
Los paquetes Microsoft.AspNetCore.Server.WebListener y Microsoft.Net.Http.Server se han combinado en un
nuevo paquete, Microsoft.AspNetCore.Server.HttpSys . Los espacios de nombres se han actualizado para que
coincidan.
Para más información, vea HTTP.sys web server implementation in ASP.NET Core (Implementaciones del servidor
web de HTTP.sys en ASP.NET Core).

Compatibilidad mejorada de los encabezados HTTP


Al usar MVC para transmitir un FileStreamResult o un FileContentResult , ahora tiene la opción de establecer una
ETag o una fecha LastModified en el contenido que se transmite. Puede establecer estos valores en el contenido
devuelto con un código similar al siguiente:

var data = Encoding.UTF8.GetBytes("This is a sample text from a binary array");


var entityTag = new EntityTagHeaderValue("\"MyCalculatedEtagValue\"");
return File(data, "text/plain", "downloadName.txt", lastModified: DateTime.UtcNow.AddSeconds(-5), entityTag:
entityTag);

Al archivo devuelto a los visitantes se incorporarán los encabezados HTTP adecuados para los valores ETag y
LastModified .

Si un visitante de la aplicación solicita el contenido con un encabezado de solicitud de intervalo, ASP.NET Core
reconoce la solicitud y controla ese encabezado. Si el contenido solicitado se puede entregar de forma parcial,
ASP.NET Core lo omite debidamente y solo devuelve el conjunto de bytes solicitado. No es necesario que escriba
ningún controlador especial en los métodos para adaptar o controlar esta característica, ya que se controla
automáticamente.

Inicio del hospedaje y Application Insights


Ahora, los entornos de hospedaje pueden insertar dependencias de paquetes adicionales y ejecutar código durante
el inicio de la aplicación sin que la aplicación tenga que tomar una dependencia explícitamente o llamar a ningún
método. Esta característica se puede usar para habilitar ciertos entornos y activar características únicas de ese
entorno sin que la aplicación tenga que saberlo de antemano.
En ASP.NET Core 2.0, esta característica se usa para habilitar automáticamente los diagnósticos de Application
Insights al efectuar una depuración en Visual Studio y (tras la participación) al ejecutarse en Azure App Services.
Como resultado, las plantillas del proyecto ya no agregan de forma predeterminada el código ni los paquetes de
Application Insights.
Para información sobre el estado de la documentación planeada, vea este problema de GitHub.

Uso automático de tokens antifalsificación


ASP.NET Core siempre ha ayudado a codificar en HTML el contenido de forma predeterminada, pero con la nueva
versión estamos dando un paso más para impedir ataques de falsificación de solicitud entre sitios (CSRF ). A partir
de ahora, ASP.NET Core emitirá tokens antifalsificación de forma predeterminada y los validará en las páginas y
acciones POST de formulario sin tener que aplicar ninguna configuración adicional.
Para más información, vea Preventing Cross-Site Request Forgery (XSRF/CSRF ) Attacks (Evitar los ataques de
falsificación de solicitud entre sitios [XSRF/CSRF ]).

Precompilación automática
La precompilación de vistas de Razor está habilitada de forma predeterminada durante la publicación, lo que
reduce el tamaño de salida de la publicación y el tiempo de inicio de la aplicación.
Para más información, vea Precompilación y compilación de vistas de Razor en ASP.NET Core.

Compatibilidad de Razor con C# 7.1


El motor de vistas de Razor se ha actualizado para poder funcionar con el nuevo compilador Roslyn. Incluye
compatibilidad con características de C# 7.1, como las expresiones predeterminadas, los nombres de tupla
inferidos y la coincidencia de patrones con genéricos. Para usar C# 7.1 en el proyecto, agregue la siguiente
propiedad al archivo del proyecto y, luego, vuelva a cargar la solución:

<LangVersion>latest</LangVersion>

Para información sobre el estado de las características de C# 7.1, vea el repositorio de GitHub para Roslyn.

Otras actualizaciones de documentación para la versión 2.0


Perfiles de publicación de Visual Studio para el desarrollo de aplicaciones ASP.NET Core
Administración de claves
Configurar la autenticación de Facebook
Configurar la autenticación de Twitter
Configurar la autenticación de Google
Configurar la autenticación de la cuenta Microsoft

Guía de migración
Para obtener instrucciones sobre cómo migrar aplicaciones de ASP.NET Core 1.x a ASP.NET Core 2.0, vea los
siguientes recursos:
Migración de ASP.NET Core 1.x a ASP.NET Core 2.0
Migrar la autenticación y la identidad a ASP.NET Core 2.0

Información adicional
Para ver la lista completa de cambios, consulte las notas de la versión de ASP.NET Core 2.0.
Para estar en contacto con el progreso y los planes del equipo de desarrollo de ASP.NET Core, sintonice ASP.NET
Community Standup.
Novedades de ASP.NET Core 1.1
27/08/2018 • 2 minutes to read • Edit Online

ASP.NET Core 1.1 incluye las siguientes características nuevas:


Middleware de reescritura de dirección URL
Middleware de almacenamiento en caché de respuestas
Componentes de vista como aplicaciones auxiliares de etiquetas
Middleware como filtros de MVC
Proveedor TempData basado en cookies
Proveedor de registros de Azure App Service
Proveedor de configuración de Azure Key Vault
Repositorios de claves de protección de datos de almacenamiento de Azure y Redis
Servidor WebListener para Windows
Compatibilidad con WebSockets

Elegir entre las versiones 1.0 y 1.1 de ASP.NET Core


ASP.NET Core 1.1 cuenta con más características que 1.0. Por lo general se recomienda usar la versión más
reciente.

Información adicional
Notas de la versión de ASP.NET Core 1.1.0
Para estar en contacto con el progreso y los planes del equipo de desarrollo de ASP.NET Core, sintonice
ASP.NET Community Standup.
Introducción a ASP.NET Core
25/07/2018 • 5 minutes to read • Edit Online

1. Instale el SDK de .NET Core 2.1 o versiones posteriores.


2. Cree un proyecto de ASP.NET Core. Abra un shell de comandos y escriba el siguiente comando:

dotnet new webapp -o aspnetcoreapp

NOTE
If the dotnet new webapp <OPTIONS> command loads the dotnet new command help instead of creating a new
Razor Pages app, install .NET Core SDK 2.1.300 or later. As of .NET Core SDK 2.1.300, the webapp Short Name is an
alias for razor .

3. Confíe en el certificado de desarrollo HTTPS:


Windows
macOS
Linux

```console
dotnet dev-certs https --trust
```

El comando anterior muestra el siguiente cuadro de diálogo:

Si acepta confiar en el certificado de desarrollo, seleccione Sí.


1. Instale el .NET Core SDK 2.0 o posterior.
2. Cree un proyecto de ASP.NET Core.
Abra un shell de comandos. Escriba el comando siguiente:

dotnet new razor -o aspnetcoreapp

3. Ejecute la aplicación con los siguientes comandos:

cd aspnetcoreapp
dotnet run

4. Vaya a http://localhost:5000.
5. Abra Pages/About.cshtml y modifique la página para que muestre el mensaje "¡ Hola, mundo!". La hora del
servidor es @DateTime.Now" :

@page
@model AboutModel
@{
ViewData["Title"] = "About";
}
<h2>@ViewData["Title"]</h2>
<h3>@Model.Message</h3>

<p>Hello, world! The time on the server is @DateTime.Now</p>

6. Vaya a http://localhost:5000/About y compruebe los cambios.


Pasos siguientes
Una aplicación de ASP.NET Core puede usar la biblioteca de clases base y el runtime de .NET Core o .NET
Framework. Para más información, vea Selección entre .NET Core y .NET Framework.
Tutoriales de introducción
Introducción a ASP.NET Core
Arquitectura y aspectos básicos de ASP.NET Core.
1. Instale el instalador del SDK de .NET Core para SDK 1.0.4 desde la página de descargas de .NET Core.
2. Cree una carpeta para un nuevo proyecto de ASP .NET Core.
Abra un shell de comandos. Escriba los siguientes comandos:

mkdir aspnetcoreapp
cd aspnetcoreapp

3. Si ha instalado una versión posterior del SDK en el equipo, cree un archivo global.json para seleccionar la
versión 1.0.4 del SDK.

{
"sdk": { "version": "1.0.4" }
}

4. Cree un proyecto de ASP.NET Core.

dotnet new web

5. Restaure los paquetes.


dotnet restore

6. Ejecute la aplicación.

dotnet run

El comando dotnet run compila primero la aplicación en caso de que sea necesario.
7. Vaya a http://localhost:5000 .
Pasos siguientes
Una aplicación de ASP.NET Core puede usar la biblioteca de clases base y el runtime de .NET Core o .NET
Framework. Para más información, vea Selección entre .NET Core y .NET Framework.
Tutoriales de introducción
Introducción a ASP.NET Core
Arquitectura y aspectos básicos de ASP.NET Core.
Introducción a las páginas de Razor en ASP.NET Core
23/08/2018 • 36 minutes to read • Edit Online

Por Rick Anderson y Ryan Nowak


Las páginas de Razor son una nueva característica de ASP.NET Core MVC que facilita la codificación de escenarios
centrados en páginas y hace que sea más productiva.
Si busca un tutorial que use el enfoque Model-View -Controller, consulte Introducción a ASP.NET Core MVC.
En este documento se proporciona una introducción a las páginas de Razor. No es un tutorial paso a paso. Si
encuentra que alguna sección es demasiado avanzada, consulte Introducción a las páginas de Razor. Para obtener
información general de ASP.NET Core, vea Introducción a ASP.NET Core.

Requisitos previos
Instale uno de los siguientes:
Herramientas de CLI: Windows, Linux o macOS: .NET Core SDK 2.0 o posterior
Herramientas de IDE/editor
Windows: Visual Studio para Windows
Carga de trabajo de ASP.NET y desarrollo web
Carga de trabajo Desarrollo multiplataforma de .NET Core
Linux: Visual Studio Code
macOS: Visual Studio para Mac

Crear un proyecto de páginas de Razor


Visual Studio
Visual Studio para Mac
Visual Studio Code
CLI de .NET Core
Vea Introducción a las páginas de Razor para obtener instrucciones detalladas sobre cómo crear un proyecto de
páginas de Razor con Visual Studio.

Páginas de Razor
Páginas de Razor está habilitada en Startup.cs:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// Includes support for Razor Pages and controllers.
services.AddMvc();
}

public void Configure(IApplicationBuilder app)


{
app.UseMvc();
}
}

Considere la posibilidad de una página básica:

@page

<h1>Hello, world!</h1>
<h2>The time on the server is @DateTime.Now</h2>

El código anterior es muy parecido a un archivo de vista de Razor. La directiva @page lo hace diferente. @page
transforma el archivo en una acción de MVC, lo que significa que administra las solicitudes directamente, sin tener
que pasar a través de un controlador. @page debe ser la primera directiva de Razor de una página. @page afecta al
comportamiento de otras construcciones de Razor.
Una página similar, con una clase PageModel , se muestra en los dos archivos siguientes. El archivo
Pages/Index2.cshtml:

@page
@using RazorPagesIntro.Pages
@model IndexModel2

<h2>Separate page model</h2>


<p>
@Model.Message
</p>

Modelo de página Pages/Index2.cshtml.cs:

using Microsoft.AspNetCore.Mvc.RazorPages;
using System;

namespace RazorPagesIntro.Pages
{
public class IndexModel2 : PageModel
{
public string Message { get; private set; } = "PageModel in C#";

public void OnGet()


{
Message += $" Server time is { DateTime.Now }";
}
}
}

Por convención, el archivo de clase PageModel tiene el mismo nombre que el archivo de páginas de Razor con .cs
anexado. Por ejemplo, la página de Razor anterior es Pages/Index2.cshtml. El archivo que contiene la clase
PageModel se denomina Pages/Index2.cshtml.cs.
Las asociaciones de rutas de dirección URL a páginas se determinan según la ubicación de la página en el sistema
de archivos. En la tabla siguiente, se muestra una ruta de acceso de página de Razor y la dirección URL
correspondiente:

RUTA DE ACCESO Y NOMBRE DE ARCHIVO URL CORRESPONDIENTE

/Pages/Index.cshtml / o /Index

/Pages/Contact.cshtml /Contact

/Pages/Store/Contact.cshtml /Store/Contact

/Pages/Store/Index.cshtml /Store o /Store/Index

Notas:
El runtime busca archivos de páginas de Razor en la carpeta Páginas de forma predeterminada.
Index es la página predeterminada cuando una URL no incluye una página.

Escribir un formulario básico


Las páginas de Razor están diseñadas para facilitar la implementación de patrones comunes que se usan con
exploradores web al compilar una aplicación. Los enlaces de modelos, las aplicaciones auxiliares de etiquetas y las
aplicaciones auxiliares de HTML simplemente funcionan con las propiedades definidas en una clase de página de
Razor. Considere la posibilidad de una página que implementa un formulario básico del estilo "Póngase en
contacto con nosotros" para el modelo Contact :
Para los ejemplos de este documento, DbContext se inicializa en el archivo Startup.cs.

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using RazorPagesContacts.Data;

namespace RazorPagesContacts
{
public class Startup
{
public IHostingEnvironment HostingEnvironment { get; }

public void ConfigureServices(IServiceCollection services)


{
services.AddDbContext<AppDbContext>(options =>
options.UseInMemoryDatabase("name"));
services.AddMvc();
}

public void Configure(IApplicationBuilder app)


{
app.UseMvc();
}
}
}

El modelo de datos:
using System.ComponentModel.DataAnnotations;

namespace RazorPagesContacts.Data
{
public class Customer
{
public int Id { get; set; }

[Required, StringLength(100)]
public string Name { get; set; }
}
}

El contexto de la base de datos:

using Microsoft.EntityFrameworkCore;

namespace RazorPagesContacts.Data
{
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions options)
: base(options)
{
}

public DbSet<Customer> Customers { get; set; }


}
}

El archivo de vista Pages/Create.cshtml:

@page
@model RazorPagesContacts.Pages.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<html>
<body>
<p>
Enter your name.
</p>
<div asp-validation-summary="All"></div>
<form method="POST">
<div>Name: <input asp-for="Customer.Name" /></div>
<input type="submit" />
</form>
</body>
</html>

Modelo de página Pages/Create.cshtml.cs:


using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesContacts.Data;

namespace RazorPagesContacts.Pages
{
public class CreateModel : PageModel
{
private readonly AppDbContext _db;

public CreateModel(AppDbContext db)


{
_db = db;
}

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

public async Task<IActionResult> OnPostAsync()


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

_db.Customers.Add(Customer);
await _db.SaveChangesAsync();
return RedirectToPage("/Index");
}
}
}

Por convención, la clase PageModel se denomina <PageName>Model y se encuentra en el mismo espacio de nombres
que la página.
La clase PageModel permite la separación de la lógica de una página de su presentación. Define los controladores
de página para solicitudes que se envían a la página y los datos que usan para representar la página. Esta
separación le permite administrar dependencias de páginas mediante la inyección de dependencias y para realizar
pruebas unitarias de las páginas.
La página tiene un método de controlador OnPostAsync , que se ejecuta en solicitudes POST (cuando un usuario
envía el formulario). Puede agregar métodos de controlador para cualquier verbo HTTP. Los controladores más
comunes son:
OnGet para inicializar el estado necesario para la página. Ejemplo OnGet.
OnPost para controlar los envíos del formulario.

El sufijo de nombre Async es opcional, pero se usa a menudo por convención para funciones asincrónicas. El
código OnPostAsync en el ejemplo anterior es similar a lo que escribiría normalmente en un controlador. El código
anterior es típico de las páginas de Razor. La mayoría de primitivos MVC como el enlace de modelos, la validación
y los resultados de acciones se comparten.
El método OnPostAsync anterior:
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}

_db.Customers.Add(Customer);
await _db.SaveChangesAsync();
return RedirectToPage("/Index");
}

El flujo básico de OnPostAsync :


Compruebe los errores de validación.
Si no hay ningún error, guarde los datos y redirija.
Si hay errores, muestre la página de nuevo con mensajes de validación. La validación del lado cliente es idéntica
a las aplicaciones de ASP.NET Core MVC tradicionales. En muchos casos, los errores de validación se detectan
en el cliente y nunca se envían al servidor.
Cuando los datos se escriben correctamente, el método del controlador OnPostAsync llama al método auxiliar
RedirectToPage para devolver una instancia de RedirectToPageResult . RedirectToPage es un resultado de acción
nueva, similar a RedirectToAction o RedirectToRoute , pero personalizada para las páginas. En el ejemplo anterior,
redirige a la página de índice raíz ( /Index ). RedirectToPage se detalla en la sección Generación de direcciones URL
para las páginas.
Cuando el formulario enviado tiene errores de validación (que se pasan al servidor), el método del controlador
OnPostAsync llama al método auxiliar Page . Page devuelve una instancia de PageResult . Devolver Page es
similar a cómo las acciones en los controladores devuelven View . PageResult es el tipo de valor devuelto
predeterminado para un método de controlador. Un método de controlador que devuelve void representa la
página.
La propiedad Customer usa el atributo [BindProperty] para participar en el enlace de modelos.

public class CreateModel : PageModel


{
private readonly AppDbContext _db;

public CreateModel(AppDbContext db)


{
_db = db;
}

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

public async Task<IActionResult> OnPostAsync()


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

_db.Customers.Add(Customer);
await _db.SaveChangesAsync();
return RedirectToPage("/Index");
}
}
De forma predeterminada, las páginas de Razor enlazan propiedades solo con verbos que no sean GET. Enlazar a
propiedades puede reducir la cantidad de código que se debe escribir. Enlazar reduce el código al usar la misma
propiedad para representar los campos de formulario ( <input asp-for="Customer.Name" /> ) y aceptar la entrada.

NOTE
Por motivos de seguridad, debe participar en el enlace de datos de solicitud GET con las propiedades del modelo de página.
Compruebe las entradas de los usuarios antes de asignarlas a las propiedades. Si participa en este comportamiento, le puede
ser útil al trabajar con escenarios que dependan de cadenas de consultas o valores de rutas.
Para enlazar una propiedad en solicitudes GET, establezca la propiedad SupportsGet del atributo [BindProperty] como
true : [BindProperty(SupportsGet = true)]

La página principal (Index.cshtml):

@page
@model RazorPagesContacts.Pages.IndexModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<h1>Contacts</h1>
<form method="post">
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
</tr>
</thead>
<tbody>
@foreach (var contact in Model.Customers)
{
<tr>
<td>@contact.Id</td>
<td>@contact.Name</td>
<td>
<a asp-page="./Edit" asp-route-id="@contact.Id">edit</a>
<button type="submit" asp-page-handler="delete"
asp-route-id="@contact.Id">delete</button>
</td>
</tr>
}
</tbody>
</table>

<a asp-page="./Create">Create</a>
</form>

La clase PageModel asociada (Index.cshtml.cs):


using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesContacts.Data;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;

namespace RazorPagesContacts.Pages
{
public class IndexModel : PageModel
{
private readonly AppDbContext _db;

public IndexModel(AppDbContext db)


{
_db = db;
}

public IList<Customer> Customers { get; private set; }

public async Task OnGetAsync()


{
Customers = await _db.Customers.AsNoTracking().ToListAsync();
}

public async Task<IActionResult> OnPostDeleteAsync(int id)


{
var contact = await _db.Customers.FindAsync(id);

if (contact != null)
{
_db.Customers.Remove(contact);
await _db.SaveChangesAsync();
}

return RedirectToPage();
}
}
}

El archivo Index.cshtml contiene el siguiente marcado para crear un vínculo de edición para cada contacto:

<a asp-page="./Edit" asp-route-id="@contact.Id">edit</a>

La aplicación auxiliar de etiquetas delimitadoras ha usado el atributo asp-route-{value} para generar un vínculo a
la página de edición. El vínculo contiene datos de ruta con el identificador del contacto. Por ejemplo:
http://localhost:5000/Edit/1 .

El archivo Pages/Edit.cshtml:
@page "{id:int}"
@model RazorPagesContacts.Pages.EditModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

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

<h1>Edit Customer - @Model.Customer.Id</h1>


<form method="post">
<div asp-validation-summary="All"></div>
<input asp-for="Customer.Id" type="hidden" />
<div>
<label asp-for="Customer.Name"></label>
<div>
<input asp-for="Customer.Name" />
<span asp-validation-for="Customer.Name" ></span>
</div>
</div>

<div>
<button type="submit">Save</button>
</div>
</form>

La primera línea contiene la directiva @page "{id:int}" . La restricción de enrutamiento "{id:int}" indica a la
página que acepte las solicitudes a la página que contienen datos de ruta int . Si una solicitud a la página no
contiene datos de ruta que se puedan convertir en int , el tiempo de ejecución devuelve un error HTTP 404 (no
encontrado). Para que el identificador sea opcional, anexe ? a la restricción de ruta:

@page "{id:int?}"

El archivo Pages/Edit.cshtml.cs:
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using RazorPagesContacts.Data;

namespace RazorPagesContacts.Pages
{
public class EditModel : PageModel
{
private readonly AppDbContext _db;

public EditModel(AppDbContext db)


{
_db = db;
}

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

public async Task<IActionResult> OnGetAsync(int id)


{
Customer = await _db.Customers.FindAsync(id);

if (Customer == null)
{
return RedirectToPage("/Index");
}

return Page();
}

public async Task<IActionResult> OnPostAsync()


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

_db.Attach(Customer).State = EntityState.Modified;

try
{
await _db.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
throw new Exception($"Customer {Customer.Id} not found!");
}

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

El archivo index.cshtml también contiene una marca para crear un botón de eliminar para cada contacto de cliente:

<button type="submit" asp-page-handler="delete"


asp-route-id="@contact.Id">delete</button>

Al representar dicho botón de eliminar en HTML, formaction incluye parámetros para:


Id. de contacto de cliente especificado mediante el atributo asp-route-id .
handler especificado mediante el atributo asp-page-handler .

Aquí tiene un ejemplo de un botón de eliminar representado con un id. de contacto de cliente de 1 :

<button type="submit" formaction="/?id=1&amp;handler=delete">delete</button>

Al seleccionar el botón, se envía una solicitud de formulario POST al servidor. De forma predeterminada, el nombre
del método de control se selecciona de acuerdo con el valor del parámetro handler y según el esquema
OnPost[handler]Async .

Como en este ejemplo handler es delete , el método de control OnPostDeleteAsync se usa para procesar la
solicitud POST . Si asp-page-handler se establece en otro valor, como remove , se seleccionará un método de control
de páginas con el nombre OnPostRemoveAsync .

public async Task<IActionResult> OnPostDeleteAsync(int id)


{
var contact = await _db.Customers.FindAsync(id);

if (contact != null)
{
_db.Customers.Remove(contact);
await _db.SaveChangesAsync();
}

return RedirectToPage();
}

El método OnPostDeleteAsync realiza las acciones siguientes:


Acepta el elemento id de la cadena de consulta.
Realiza una consulta a la base de datos del contacto de cliente con FindAsync .
Si encuentra dicho contacto de cliente, se quita de la lista correspondiente. Luego, se actualiza la base de datos.
Llama a RedirectToPage para redirigir la página Index raíz ( /Index ).

Es necesario marcar las propiedades de página


Las propiedades de un valor PageModel se pueden decorar con el atributo Required:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.ComponentModel.DataAnnotations;

namespace RazorPagesMovie.Pages.Movies
{
public class CreateModel : PageModel
{
public IActionResult OnGet()
{
return Page();
}

[BindProperty]
[Required(ErrorMessage = "Color is required")]
public string Color { get; set; }

public IActionResult OnPostAsync()


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

// Process color.

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

Para más información, vea Validación de modelos.

Administración de solicitudes HEAD con el controlador OnGet


Normalmente, se crea un controlador HEAD al que se llama para las solicitudes HEAD:

public void OnHead()


{
HttpContext.Response.Headers.Add("HandledBy", "Handled by OnHead!");
}

Si no se define ningún controlador HEAD ( OnHead ), las páginas de Razor vuelven a llamar al controlador de
páginas GET ( OnGet ) en ASP.NET Core 2.1 o posterior. Tiene la opción de usar este comportamiento con el
método SetCompatibilityVersion en Startup.Configure para ASP.NET Core 2.1 a 2.x:

services.AddMvc()
.SetCompatibilityVersion(Microsoft.AspNetCore.Mvc.CompatibilityVersion.Version_2_1);

SetCompatibilityVersion define con eficacia la opción de las páginas de Razor


AllowMappingHeadRequestsToGetHandler como true .

En lugar de participar en todos los comportamientos de 2.1 con SetCompatibilityVersion , puede participar
explícitamente en comportamientos específicos. El código que se indica a continuación participa en las solicitudes
HEAD de asignación del controlador GET.
services.AddMvc()
.AddRazorPagesOptions(options =>
{
options.AllowMappingHeadRequestsToGetHandler = true;
});

XSRF/CSRF y páginas de Razor


No tiene que escribir ningún código para la validación antifalsificación. La validación y generación de tokens
antifalsificación se incluyen automáticamente en las páginas de Razor.

Usar diseños, parciales, plantillas y aplicaciones auxiliares de etiquetas


con las páginas de Razor
Las páginas funcionan con todas las características del motor de vista de Razor. Los diseños, parciales, plantillas,
aplicaciones auxiliares de etiquetas, _ViewStart.cshtml, _ViewImports.cshtml funcionan de la misma manera que lo
hacen con las vistas de Razor convencionales.
Para simplificar esta página, aprovecharemos algunas de esas características.
Agregue una página de diseño a Pages/Shared/_Layout.cshtml:
Agregue una página de diseño a Pages/_Layout.cshtml:

<!DOCTYPE html>
<html>
<head>
<title>Razor Pages Sample</title>
</head>
<body>
<a asp-page="/Index">Home</a>
@RenderBody()
<a asp-page="/Customers/Create">Create</a> <br />
</body>
</html>

El diseño:
Controla el diseño de cada página (a no ser que la página no tenga diseño).
Importa las estructuras HTML como JavaScript y hojas de estilos.
Vea Layout page (Página de diseño) para obtener más información.
La propiedad Layout se establece en Pages/_ViewStart.cshtml:

@{
Layout = "_Layout";
}

El diseño está en la carpeta Pages/Shared. Las páginas buscan otras vistas (diseños, plantillas, parciales) de forma
jerárquica, a partir de la misma carpeta que la página actual. Un diseño en la carpeta Pages/Shared se puede usar
desde cualquier página de Razor en la carpeta Pages.
El archivo de diseño debería ir en la carpeta Pages/Shared.
El diseño está en la carpeta Pages. Las páginas buscan otras vistas (diseños, plantillas, parciales) de forma
jerárquica, a partir de la misma carpeta que la página actual. Un diseño en la carpeta Pages se puede usar desde
cualquier página de Razor en la carpeta Pages.
Le recomendamos que no coloque el archivo de diseño en la carpeta Views/Shared. Views/Shared es un patrón de
vistas de MVC. Las páginas de Razor están diseñadas para basarse en la jerarquía de carpetas, no en las
convenciones de ruta de acceso.
La búsqueda de vistas de una página de Razor incluye la carpeta Pages. Los diseños, plantillas y parciales que usa
con los controladores de MVC y las vistas de Razor convencionales simplemente funcionan.
Agregue un archivo Pages/_ViewImports.cshtml:

@namespace RazorPagesContacts.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

@namespace se explica más adelante en el tutorial. La directiva @addTagHelper pone las aplicaciones auxiliares de
etiquetas integradas en todas las páginas de la carpeta Pages.
Cuando la directiva @namespace se usa explícitamente en una página:

@page
@namespace RazorPagesIntro.Pages.Customers

@model NameSpaceModel

<h2>Name space</h2>
<p>
@Model.Message
</p>

La directiva establece el espacio de nombres de la página. La directiva @model no necesita incluir el espacio de
nombres.
Cuando la directiva @namespace se encuentra en _ViewImports.cshtml, el espacio de nombres especificado
proporciona el prefijo del espacio de nombres generado en la página que importa la directiva @namespace . El resto
del espacio de nombres generado (la parte del sufijo) es la ruta de acceso relativa separada por puntos entre la
carpeta que contiene _ViewImports.cshtml y la carpeta que contiene la página.
Por ejemplo, la clase PageModel Pages/Customers/Edit.cshtml.cs establece explícitamente el espacio de nombres:

namespace RazorPagesContacts.Pages
{
public class EditModel : PageModel
{
private readonly AppDbContext _db;

public EditModel(AppDbContext db)


{
_db = db;
}

// Code removed for brevity.

El archivo Pages/_ViewImports.cshtml establece el espacio de nombres siguiente:

@namespace RazorPagesContacts.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

El espacio de nombres generado para la página de Razor Pages/Customers/Edit.cshtml es el mismo que la clase
PageModel .
@namespace también funciona con las vistas de Razor convencionales.
El archivo de vista Pages/Create.cshtml original:

@page
@model RazorPagesContacts.Pages.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<html>
<body>
<p>
Enter your name.
</p>
<div asp-validation-summary="All"></div>
<form method="POST">
<div>Name: <input asp-for="Customer.Name" /></div>
<input type="submit" />
</form>
</body>
</html>

El archivo de vista Pages/Create.cshtml actualizado:

@page
@model CreateModel

<html>
<body>
<p>
Enter your name.
</p>
<div asp-validation-summary="All"></div>
<form method="POST">
<div>Name: <input asp-for="Customer.Name" /></div>
<input type="submit" />
</form>
</body>
</html>

El proyecto de inicio de las páginas de Razor contiene Pages/_ValidationScriptsPartial.cshtml, que enlaza la


validación del lado cliente.

Generación de direcciones URL para las páginas


La página Create , mostrada anteriormente, usa RedirectToPage :

public async Task<IActionResult> OnPostAsync()


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

_db.Customers.Add(Customer);
await _db.SaveChangesAsync();
return RedirectToPage("/Index");
}

La aplicación tiene la siguiente estructura de archivos o carpetas:


/Pages
Index.cshtml
/Customers
Create.cshtml
Edit.cshtml
Index.cshtml
Las páginas Pages/Customers/Create.cshtml y Pages/Customers/Edit.cshtml redirigen a Pages/Index.cshtml si se
realiza correctamente. La cadena /Index forma parte del URI para tener acceso a la página anterior. La cadena
/Index puede usarse para generar los URI para la página Pages/Index.cshtml. Por ejemplo:

Url.Page("/Index", ...)
<a asp-page="/Index">My Index Page</a>
RedirectToPage("/Index")

El nombre de página es la ruta de acceso a la página de la carpeta raíz /Pages, incluido un / inicial, por ejemplo
/Index . Los ejemplos anteriores de generación de URL ofrecen opciones mejoradas y capacidades funcionales en
comparación con la escritura a mano de estas. La generación de direcciones URL usa el enrutamiento y puede
generar y codificar parámetros según cómo se defina la ruta en la ruta de acceso de destino.
La generación de direcciones URL para las páginas admite nombres relativos. En la siguiente tabla, se muestra qué
página de índice está seleccionada con diferentes parámetros RedirectToPage de Pages/Customers/Create.cshtml:

REDIRECTTOPAGE(X) PÁGINA

RedirectToPage("/Index") Pages/Index

RedirectToPage("./Index"); Pages/Customers/Index

RedirectToPage("../Index") Pages/Index

RedirectToPage("Index") Pages/Customers/Index

RedirectToPage("Index") , RedirectToPage("./Index")y RedirectToPage("../Index") son nombres relativos. El


parámetro RedirectToPage se combina con la ruta de acceso de la página actual para calcular el nombre de la
página de destino.
Vincular el nombre relativo es útil al crear sitios con una estructura compleja. Si usa nombres relativos para
vincular entre páginas en una carpeta, puede cambiar el nombre de esa carpeta. Todos los vínculos seguirán
funcionando (porque no incluyen el nombre de carpeta).

Atributo ViewData
Se pueden pasar datos a una página con ViewDataAttribute. Los valores de las propiedades de controladores o
modelos de página de Razor decoradas con [ViewData] se almacenan y cargan desde ViewDataDictionary.
En el ejemplo siguiente, el valor AboutModel contiene una propiedad Title decorada con [ViewData] . La
propiedad Title se establece en el título de la página Acerca de:
public class AboutModel : PageModel
{
[ViewData]
public string Title { get; } = "About";

public void OnGet()


{
}
}

En la página Acerca de, acceda 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>
...

TempData
ASP.NET Core 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 útil para
el redireccionamiento cuando se necesitan los datos de más de una única solicitud.
El atributo [TempData] es nuevo en ASP.NET Core 2.0 y es compatible con controladores y páginas.
El siguiente código establece el valor de Message mediante TempData :
public class CreateDotModel : PageModel
{
private readonly AppDbContext _db;

public CreateDotModel(AppDbContext db)


{
_db = db;
}

[TempData]
public string Message { get; set; }

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

public async Task<IActionResult> OnPostAsync()


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

_db.Customers.Add(Customer);
await _db.SaveChangesAsync();
Message = $"Customer {Customer.Name} added";
return RedirectToPage("./Index");
}
}

El siguiente marcado en el archivo Pages/Customers/Index.cshtml muestra el valor de Message mediante TempData


.

<h3>Msg: @Model.Message</h3>

El modelo de página Pages/Customers/Index.cshtml.cs aplica el atributo [TempData] a la propiedad Message .

[TempData]
public string Message { get; set; }

Vea TempData para obtener más información.

Varios controladores por página


La siguiente página genera marcado para dos controladores de páginas mediante la aplicación auxiliar de etiquetas
asp-page-handler :
@page
@model CreateFATHModel

<html>
<body>
<p>
Enter your name.
</p>
<div asp-validation-summary="All"></div>
<form method="POST">
<div>Name: <input asp-for="Customer.Name" /></div>
<input type="submit" asp-page-handler="JoinList" value="Join" />
<input type="submit" asp-page-handler="JoinListUC" value="JOIN UC" />
</form>
</body>
</html>

El formulario del ejemplo anterior tiene dos botones de envío, y cada uno de ellos usa FormActionTagHelper para
enviar a una dirección URL diferente. El atributo asp-page-handler es un complemento de asp-page .
asp-page-handler genera direcciones URL que envían a cada uno de los métodos de controlador definidos por una
página. asp-page no se especifica porque el ejemplo se vincula a la página actual.
Modelo de página:
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesContacts.Data;

namespace RazorPagesContacts.Pages.Customers
{
public class CreateFATHModel : PageModel
{
private readonly AppDbContext _db;

public CreateFATHModel(AppDbContext db)


{
_db = db;
}

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

public async Task<IActionResult> OnPostJoinListAsync()


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

_db.Customers.Add(Customer);
await _db.SaveChangesAsync();
return RedirectToPage("/Index");
}

public async Task<IActionResult> OnPostJoinListUCAsync()


{
if (!ModelState.IsValid)
{
return Page();
}
Customer.Name = Customer.Name?.ToUpper();
return await OnPostJoinListAsync();
}
}
}

El código anterior usa métodos de controlador con nombre. Los métodos de controlador con nombre se crean
tomando el texto en el nombre después de On<HTTP Verb> y antes de Async (si existe). En el ejemplo anterior, los
métodos de página son OnPostJoinListAsync y OnPostJoinListUCAsync. Quitando OnPost y Async, los nombres
de controlador son JoinList y JoinListUC .

<input type="submit" asp-page-handler="JoinList" value="Join" />


<input type="submit" asp-page-handler="JoinListUC" value="JOIN UC" />

Al usar el código anterior, la ruta de dirección URL que envía a OnPostJoinListAsync es


http://localhost:5000/Customers/CreateFATH?handler=JoinList . La ruta de dirección URL que envía a
OnPostJoinListUCAsync es http://localhost:5000/Customers/CreateFATH?handler=JoinListUC .

Rutas personalizadas
Use la directiva @page para:
Especificar una ruta personalizada a una página. Por ejemplo, la ruta a la página Acerca de se puede establecer
en /Some/Other/Path con @page "/Some/Other/Path" .
Anexar segmentos a la ruta predeterminada de una página. Por ejemplo, se puede agregar un segmento "item"
a la ruta predeterminada de una página con @page "item" .
Anexar parámetros a la ruta predeterminada de una página. Por ejemplo, un parámetro de identificador, id ,
puede ser necesario para una página con @page "{id}" .

Se admite una ruta de acceso relativa raíz designada por una tilde ( ~ ) al principio de la ruta de acceso. Por
ejemplo, @page "~/Some/Other/Path" es lo mismo que @page "/Some/Other/Path" .
La cadena de consulta ?handler=JoinList de la dirección URL se puede cambiar por un segmento de ruta
/JoinList , para lo cual hay que especificar la plantilla de ruta @page "{handler?}" .

Si no le gusta la cadena de consulta ?handler=JoinList en la dirección URL, puede cambiar la ruta para poner el
nombre del controlador en la parte de la ruta de la dirección URL. Para personalizar la ruta, se puede agregar una
plantilla de ruta entre comillas dobles después de la directiva @page .

@page "{handler?}"
@model CreateRouteModel

<html>
<body>
<p>
Enter your name.
</p>
<div asp-validation-summary="All"></div>
<form method="POST">
<div>Name: <input asp-for="Customer.Name" /></div>
<input type="submit" asp-page-handler="JoinList" value="Join" />
<input type="submit" asp-page-handler="JoinListUC" value="JOIN UC" />
</form>
</body>
</html>

Al usar el código anterior, la ruta de dirección URL que envía a OnPostJoinListAsync es


http://localhost:5000/Customers/CreateFATH/JoinList . La ruta de dirección URL que envía a OnPostJoinListUCAsync
es http://localhost:5000/Customers/CreateFATH/JoinListUC .
El signo ? que sigue a handler significa que el parámetro de ruta es opcional.

Valores de configuración
Para configurar opciones avanzadas, use el método de extensión AddRazorPagesOptions en el generador de MVC:

public void ConfigureServices(IServiceCollection services)


{
services.AddMvc()
.AddRazorPagesOptions(options =>
{
options.RootDirectory = "/MyPages";
options.Conventions.AuthorizeFolder("/MyPages/Admin");
});
}

Actualmente, puede usar RazorPagesOptions para establecer el directorio raíz de páginas, o agregar convenciones
de modelo de aplicación a las páginas. En el futuro habilitaremos más extensibilidad de este modo.
Para precompilar vistas, vea Razor view compilation (Compilación de vistas de Razor).
Descargue o vea el código de ejemplo.
Vea Introducción a las páginas de Razor, que se basa en esta introducción.
Especificar que las páginas de Razor se encuentran en la raíz de contenido
De forma predeterminada, la ruta raíz de las páginas de Razor es el directorio /Pages. Agregue
WithRazorPagesAtContentRoot a AddMvc para especificar que las páginas de Razor están en la ruta raíz de
contenido (ContentRootPath) de la aplicación:

services.AddMvc()
.AddRazorPagesOptions(options =>
{
...
})
.WithRazorPagesAtContentRoot();

Especificar que las páginas de Razor se encuentran en un directorio raíz personalizado


Agregue WithRazorPagesRoot a AddMvc para especificar que las páginas de Razor están en un directorio raíz
personalizado en la aplicación (proporcione una ruta de acceso relativa):

services.AddMvc()
.AddRazorPagesOptions(options =>
{
...
})
.WithRazorPagesRoot("/path/to/razor/pages");

Vea también
Introducción a ASP.NET Core
Sintaxis de Razor
Introducción a las páginas de Razor
Convenciones de autorización de las páginas de Razor
Proveedores personalizados de rutas y modelos de página de páginas de Razor
Pruebas unitarias de páginas de Razor
Crear una API web con ASP.NET Core y Visual Studio
14/08/2018 • 28 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 en 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 CLR 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 2017 versión 15.7.3 o posterior con las cargas de trabajo siguientes:
Desarrollo de ASP.NET y web
Desarrollo multiplataforma de .NET Core
SDK de .NET Core 2.1 o versiones posteriores

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:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using TodoApi.Models;

namespace TodoApi
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<TodoContext>(opt =>
opt.UseInMemoryDatabase("TodoList"));
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}

public void Configure(IApplicationBuilder app)


{
app.UseMvc();
}
}
}

using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using TodoApi.Models;

namespace TodoApi
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<TodoContext>(opt =>
opt.UseInMemoryDatabase("TodoList"));
services.AddMvc();
}

public void Configure(IApplicationBuilder app)


{
app.UseMvc();
}
}
}

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:

using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Linq;
using TodoApi.Models;

namespace TodoApi.Controllers
{
[Route("api/[controller]")]
public class TodoController : ControllerBase
{
private readonly TodoContext _context;

public TodoController(TodoContext context)


{
_context = context;

if (_context.TodoItems.Count() == 0)
{
_context.TodoItems.Add(new TodoItem { Name = "Item1" });
_context.SaveChanges();
}
}
}
}

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.
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Linq;
using TodoApi.Models;

namespace TodoApi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class TodoController : ControllerBase
{
private readonly TodoContext _context;

public TodoController(TodoContext context)


{
_context = context;

if (_context.TodoItems.Count() == 0)
{
// Create a new TodoItem if collection is empty,
// which means you can't delete all TodoItems.
_context.TodoItems.Add(new TodoItem { Name = "Item1" });
_context.SaveChanges();
}
}
}
}

El código anterior:
Define una clase de controlador de API sin métodos.
Crea una tarea pendiente cuando TodoItems está vacío. No podrá eliminar todas las tareas pendientes porque el
constructor crea una si TodoItems está vacío.
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 :
[HttpGet]
public List<TodoItem> GetAll()
{
return _context.TodoItems.ToList();
}

[HttpGet("{id}", Name = "GetTodo")]


public IActionResult GetById(long id)
{
var item = _context.TodoItems.Find(id);
if (item == null)
{
return NotFound();
}
return Ok(item);
}

[HttpGet]
public ActionResult<List<TodoItem>> GetAll()
{
return _context.TodoItems.ToList();
}

[HttpGet("{id}", Name = "GetTodo")]


public ActionResult<TodoItem> GetById(long id)
{
var item = _context.TodoItems.Find(id);
if (item == null)
{
return NotFound();
}
return item;
}

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:
namespace TodoApi.Controllers
{
[Route("api/[controller]")]
public class TodoController : ControllerBase
{
private readonly TodoContext _context;

namespace TodoApi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class TodoController : ControllerBase
{
private readonly TodoContext _context;

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.

[HttpGet("{id}", Name = "GetTodo")]


public IActionResult GetById(long id)
{
var item = _context.TodoItems.Find(id);
if (item == null)
{
return NotFound();
}
return Ok(item);
}

[HttpGet("{id}", Name = "GetTodo")]


public ActionResult<TodoItem> GetById(long id)
{
var item = _context.TodoItems.Find(id);
if (item == null)
{
return NotFound();
}
return item;
}

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 :

[HttpPost]
public IActionResult Create([FromBody] TodoItem item)
{
if (item == null)
{
return BadRequest();
}

_context.TodoItems.Add(item);
_context.SaveChanges();

return CreatedAtRoute("GetTodo", new { id = item.Id }, item);


}

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.

[HttpPost]
public IActionResult Create(TodoItem item)
{
_context.TodoItems.Add(item);
_context.SaveChanges();

return CreatedAtRoute("GetTodo", new { id = item.Id }, item);


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

[HttpGet("{id}", Name = "GetTodo")]


public IActionResult GetById(long id)
{
var item = _context.TodoItems.Find(id);
if (item == null)
{
return NotFound();
}
return Ok(item);
}

[HttpGet("{id}", Name = "GetTodo")]


public ActionResult<TodoItem> GetById(long id)
{
var item = _context.TodoItems.Find(id);
if (item == null)
{
return NotFound();
}
return item;
}

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 :

[HttpPut("{id}")]
public IActionResult Update(long id, [FromBody] TodoItem item)
{
if (item == null || item.Id != id)
{
return BadRequest();
}

var todo = _context.TodoItems.Find(id);


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

todo.IsComplete = item.IsComplete;
todo.Name = item.Name;

_context.TodoItems.Update(todo);
_context.SaveChanges();
return NoContent();
}
[HttpPut("{id}")]
public IActionResult Update(long id, TodoItem item)
{
var todo = _context.TodoItems.Find(id);
if (todo == null)
{
return NotFound();
}

todo.IsComplete = item.IsComplete;
todo.Name = item.Name;

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

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>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')[0].checked = 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
29/09/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
Crear una aplicación web de SignalR en tiempo real
Crear una aplicación web de SignalR con TypeScript
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 asistentes 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
16/07/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
Después del tutorial para agregar una funcionalidad de carga de archivos a la aplicación de ejemplo, consulte
Cargar archivos en una página de Razor en ASP.NET Core.
Introducción a las páginas de Razor en ASP.NET
Core
27/08/2018 • 13 minutes to read • Edit Online

Por Rick Anderson


Se recomienda seguir la versión de ASP.NET Core 2.1 de este tutorial, ya que es mucho más fácil de
seguir y abarca más características. Seleccione ASP.NET Core 2.1 en el selector de versión.

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 2017 versión 15.7.3 o posterior con las cargas de trabajo siguientes:
Desarrollo de ASP.NET y web
Desarrollo multiplataforma de .NET Core
SDK de .NET Core 2.1 o versiones posteriores

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.1 en la lista desplegable y, luego, Aplicación web.

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. Seleccione Aceptar para dar su consentimiento al seguimiento. Esta aplicación no lleva un
seguimiento de la información personal. El código generado con plantilla incluye activos que sirven para
cumplir el Reglamento general de protección de datos (RGPD ).

En la siguiente imagen se muestra la aplicación tras haber aceptado el seguimiento:


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 recursos estáticos. Vea Archivos estáticos.

Páginas Carpeta para páginas de Razor.

appsettings.json Configuración

Program.cs Configura el host de la aplicación ASP.NET Core.

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


Consulte Inicio.

La carpeta Pages/Shared
El archivo _Layout.cshtml contiene elementos HTML comunes (vínculos de scripts y hojas de estilos) y
establece el diseño de la aplicación. Por ejemplo, cuando se selecciona RazorPagesMovie, Home, About
o Contact, aparece un conjunto común de elementos en la página web. Los elementos comunes incluyen
el menú de navegación de la parte superior y el encabezado de la parte inferior de la ventana. Para obtener
más información, vea Diseño.
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 usa el archivo
_ValidationScriptsPartial.cshtml.
El archivo _CookieConsentPartial.cshtml proporciona una barra de navegación y contenido para resumir la
directiva de privacidad y uso de cookies. Para obtener más información sobre los activos del RGPD que se
incluyen en el proyecto, consulte Compatibilidad con el Reglamento general de protección de datos
(RGPD ) de la UE.
Carpeta Páginas
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.
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.

Use F7 to toggle between a Razor Page and the PageModel


To enable F7 toggling between a Razor Page (*.cshtml file) and the C# file (*.cshtml.cs):
Select Tools > Options > Environment > Keyboard
Enter ToggleRazorView in Show commands containing.
Select EditorContextMenus.CodeWindow.ToggleRazorView
In the Press shortcut keys entry box, press F7.
Select Assign > OK

Requisitos previos
Visual Studio 2017 versión 15.7.3 o posterior con las cargas de trabajo siguientes:
Desarrollo de ASP.NET y web
Desarrollo multiplataforma de .NET Core
SDK de .NET Core 2.1 o versiones posteriores

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
24/09/2018 • 17 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:
Reemplace el contenido de la clase Movie por el siguiente código:

using System;
using System.ComponentModel.DataAnnotations.Schema;

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

Aplicar scaffolding al modelo de película


En esta sección se aplica scaffolding al modelo de película; es decir, la herramienta de scaffolding genera páginas
para las operaciones de creación, lectura, actualización y eliminación (CRUD ) del modelo de película.
Cree una carpeta Pages/Movies:
En el Explorador de soluciones, haga clic con el botón derecho en la carpeta Pages > Agregar > Nueva
carpeta.
Asigne a la carpeta el nombre Movies.
En el Explorador de soluciones, haga clic con el botón derecho en la carpeta Pages/Movies > Agregar >
Nuevo elemento con scanffold.

En el cuadro de diálogo Agregar scaffold, seleccione Razor Pages using Entity Framework (CRUD ) [Páginas
de Razor Pages que usan Entity Framework (CRUD )] > AGREGAR.
Complete el cuadro de diálogo para agregar páginas de Razor Pages que usan Entity Framework (CRUD ):
En la lista desplegable Clase de modelo, seleccione Movie (RazorPagesMovie.Models).
En la fila Clase de contexto de datos, seleccione el signo más +, inicie sesión y acepte el nombre generado
RazorPagesMovie.Models.RazorPagesMovieContext.
En la lista desplegable Clase de contexto de datos, seleccione
RazorPagesMovie.Models.RazorPagesMovieContext.
Seleccione Agregar.

El proceso de scaffolding ha creado y cambiado los archivos siguientes:


Archivos creados
Pages/Movies Create, Delete, Details, Edit, Index. Estas páginas se detallan en el tutorial siguiente.
Data/RazorPagesMovieContext.cs
Actualizaciones de archivos
Startup.cs: en la sección siguiente se detallan los cambios realizados en este archivo.
appsettings.json: se agrega la cadena de conexión que se usa para conectarse a una base de datos local.

Examinar el contexto registrado con la inserción de dependencias


ASP.NET Core integra 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.
La herramienta de scaffolding ha creado un contexto de base de datos de forma automática y lo ha registrado con
el contenedor de inserción de dependencias.
Examine el método Startup.ConfigureServices . El proveedor de scaffolding ha agregado la línea resaltada:
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});

services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

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

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

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;

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

public DbSet<RazorPagesMovie.Models.Movie> Movie { get; set; }


}
}

En el código anterior se crea una propiedad DbSet<Movie> para el conjunto de entidades. En la terminología de
Entity Framework, un conjunto de entidades suele corresponder a una tabla de base de datos. Una entidad se
corresponde con una fila de la tabla.
El nombre de la cadena de conexión se pasa al contexto mediante una llamada a un método en un objeto
DbContextOptions. Para el desarrollo local, el sistema de configuración de ASP.NET Core lee la cadena de
conexión desde el archivo appsettings.json.

Realizar la migración inicial


En esta sección, usará la Consola del Administrador de paquetes (PMC ) para:
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:

Add-Migration Initial
Update-Database

Como alternativa, se pueden usar los siguientes comandos de la CLI de .NET Core de la carpeta de proyecto:

dotnet ef migrations add Initial


dotnet ef database update

Pase por alto el siguiente mensaje de advertencia, se corregirá en un próximo tutorial:


Microsoft.EntityFrameworkCore.Model.Validation[30000]

*No type was specified for the decimal column 'Price' on entity type 'Movie'. This will cause values to be
silently truncated if they do not fit in the default precision and scale. Explicitly specify the SQL server
column type that can accommodate all the values using 'ForHasColumnType()'.*

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 RazorPagesMovieContext (en el archivo Data/RazorPagesMovieContext.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/{time-stamp }_InitialCreate.cs, con lo
que se crea la base de datos.
Si se produce un error:
SqlException: No se puede abrir la base de datos "RazorPagesMovieContext-GUID" solicitada por el inicio de
sesión. Error de inicio de sesión. Error de inicio de sesión del usuario .
Quiere decir que falta el paso de migraciones.
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

Pase por alto el siguiente mensaje:

`Microsoft.EntityFrameworkCore.Model.Validation[30000]`

*No type was specified for the decimal column 'Price' on entity type 'Movie'. This will cause values to be
silently truncated if they do not fit in the default precision and scale. Explicitly specify the SQL server
column type that can accommodate all the values using 'ForHasColumnType()'*

Lo subsanaremos en el siguiente tutorial.


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 restore
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 DESCRIPCIÓN

-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, confirme que ha ejecutado 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
06/08/2018 • 14 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:

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

namespace RazorPagesMovie.Pages.Movies
{
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 async Task OnGetAsync()


{
Movie = await _context.Movie.ToListAsync();
}
}
}
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Models;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace RazorPagesMovie.Pages.Movies
{
public class IndexModel : PageModel
{
private readonly RazorPagesMovie.Models.RazorPagesMovieContext _context;

public IndexModel(RazorPagesMovie.Models.RazorPagesMovieContext context)


{
_context = context;
}

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

public async Task OnGetAsync()


{
Movie = await _context.Movie.ToListAsync();
}
}
}

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 el siguiente asistente de HTML:
@Html.DisplayNameFor(model => model.Movie[0].Title))

El asistente de 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
los asistentes de 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/Shared/_Layout.cshtml. En el siguiente marcado se muestran las
primeras líneas del archivo Pages/Shared/_Layout.cshtml.
La propiedad "Title" se usa en el archivo Pages/Shared/_Layout.cshtml. En el siguiente marcado se muestran las
primeras líneas del archivo _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.

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.

La propiedad Layout se establece en el archivo Pages/_ViewStart.cshtml:

@{
Layout = "_Layout";
}

El marcado anterior establece el archivo de diseño en Pages/Shared/_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/Shared/_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 un asistente de etiquetas. En este caso, se trata de el asistente de etiquetas
Anchor. El atributo y valor del asistente 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");
}
}
}
// 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.RazorPagesMovieContext _context;

public CreateModel(RazorPagesMovie.Models.RazorPagesMovieContext 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 que
inicializar, de modo que se devuelve Page . Más adelante en el tutorial veremos el estado de inicialización del
método OnGet . 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
24/09/2018 • 7 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();
}

public void ConfigureServices(IServiceCollection services)


{
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});

services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

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

Para más información sobre los modelos empleados en ConfigureServices , vea:


Compatibilidad con el Reglamento general de protección de datos (RGPD ) en ASP.NET Core para
CookiePolicyOptions .
SetCompatibilityVersion
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. El valor de nombre de la base de datos (
Database={Database name} ) será distinto en su código generado. El valor de nombre es arbitrario.

"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();
}
}
}
}
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 RazorPagesMovieContext(
serviceProvider.GetRequiredService<DbContextOptions<RazorPagesMovieContext>>()))
{
// 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
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 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();
}
}
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 = CreateWebHostBuilder(args).Build();

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


{
var services = scope.ServiceProvider;

try
{
var context = services.GetRequiredService<RazorPagesMovieContext>();
context.Database.Migrate();
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 IWebHostBuilder CreateWebHostBuilder(string[] args) =>


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

Una aplicación de producción no llamaría a Database.Migrate . Se agrega al código anterior para evitar que se
produzca la siguiente excepción cuando Update-Database no se ha ejecutado:
SqlException: No se puede abrir la base de datos "RazorPagesMovieContext-21" solicitada por el inicio de sesión.
Error de inicio de sesión. Error de inicio de sesión del usuario .
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
24/09/2018 • 9 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; }
}
}

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

[Column(TypeName = "decimal(18, 2)")]


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; .
Haga clic con el botón derecho en una línea ondulada roja > Acciones rápidas y refactorizaciones en el
atributo [Column] y seleccione using System.ComponentModel.DataAnnotations.Schema; .
La anotación de datos [Column(TypeName = "decimal(18, 2)")] es necesaria para que Entity Framework Core
asigne correctamente Price a la moneda en la base de datos. Para más información, vea Tipos de datos.
Modelo completado:

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

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

[Column(TypeName = "decimal(18, 2)")]


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

Los asistentes 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:
public class EditModel : PageModel
{
private readonly RazorPagesMovie.Models.MovieContext _context;

public EditModel(RazorPagesMovie.Models.MovieContext context)


{
_context = context;
}

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

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


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

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

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

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");
}
}
public class EditModel : PageModel
{
private readonly RazorPagesMovie.Models.RazorPagesMovieContext _context;

public EditModel(RazorPagesMovie.Models.RazorPagesMovieContext context)


{
_context = context;
}

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

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


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

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

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

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

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
24/09/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 el asistente 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; }

public class IndexModel : PageModel


{
private readonly RazorPagesMovie.Models.RazorPagesMovieContext _context;

public IndexModel(RazorPagesMovie.Models.RazorPagesMovieContext 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
Agregar un campo nuevo a una página de Razor en
ASP.NET Core
24/09/2018 • 8 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.
Al usar EF Code First para crear una base de datos automáticamente, Code First hace lo siguiente:
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 las clases del modelo no están sincronizadas con la base de datos, EF produce una excepción.
La comprobación automática de la sincronización del esquema/modelo 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; }
}

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

[Column(TypeName = "decimal(18, 2)")]


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
@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>
<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 asistentes 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.


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
24/09/2018 • 16 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; }
}
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)]
[Column(TypeName = "decimal(18, 2)")]
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
debe comenzar por una o varias letras en mayúscula y continuar con cero o más letras, comillas simples o
dobles, caracteres de espacio en blanco o guiones. Rating debe comenzar por una o varias letras en
mayúscula y continuar con cero o más letras, números, comillas simples o dobles, caracteres de espacio en
blanco o guiones.
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>

El asistente de etiquetas de entrada usa los atributos DataAnnotations y genera los atributos HTML necesarios
para la validación de jQuery en el cliente. El asistente 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.
La anotación de datos [Column(TypeName = "decimal(18, 2)")] es necesaria para que Entity Framework Core
asigne correctamente Price a la moneda en la base de datos. Para más información, vea Tipos de datos.
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; }
}

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


[Column(TypeName = "decimal(18, 2)")]
public decimal Price { get; set; }

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

En Get started with Razor Pages and EF Core (Introducción a Razor Pages y EF Core) se muestran operaciones
avanzadas de EF Core con Razor Pages.
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.
Gracias por seguir esta introducción a las páginas de Razor. Le agradeceremos cualquier comentario. Introducción
a MVC con Razor Pages y EF Core es un excelente artículo de seguimiento de este tutorial.

Recursos adicionales
Trabajar con formularios
Globalización y localización
Introducción a los asistentes de etiquetas
Creación de asistentes de etiquetas

A N T E R IO R : A D IC IÓ N D E U N N U E V O
CAM PO
Tutorial: Introducción a SignalR en ASP.NET Core
24/09/2018 • 11 minutes to read • Edit Online

En este tutorial se describen los conceptos básicos de la creación de una aplicación en tiempo real con SignalR.
Aprenderá a:
Crear una aplicación web en la que se usa SignalR en ASP.NET Core.
Crear un concentrador SignalR en el servidor.
Conectarse con el concentrador SignalR desde clientes de JavaScript.
Usar el concentrador para enviar mensajes desde cualquier cliente a todos los clientes conectados.
Al final, tendrá una aplicación de chat funcional:

Vea o descargue el código de ejemplo (cómo descargarlo).

Requisitos previos
Visual Studio
Visual Studio Code
Visual Studio para Mac
Visual Studio 2017 versión 15.8 o posterior con la carga de trabajo ASP.NET y desarrollo web
.NET Core SDK 2.1 o posterior

Crear el proyecto
Visual Studio
Visual Studio Code
Visual Studio para Mac
En el menú, seleccione Archivo > Nuevo proyecto.
En el cuadro de diálogo Nuevo proyecto, seleccione Instalado > Visual C# > Web > Aplicación web
ASP.NET Core. Asigne al proyecto el nombre SignalRChat.

Seleccione Aplicación web para crear un proyecto en el que se use Razor Pages.
Seleccione una plataforma de destino de .NET Core, seleccione ASP.NET Core 2.1 y haga clic en Aceptar.

Agregar la biblioteca cliente de SignalR


La biblioteca de servidor de SignalR se incluye en el metapaquete Microsoft.AspNetCore.App. La biblioteca cliente
de JavaScript no se incluye automáticamente en el proyecto. En este tutorial, usará el Administrador de bibliotecas
(LibMan) para obtener la biblioteca cliente de unpkg. unpkg es una red de entrega de contenido que puede
entregar todo lo que encuentre en npm, el Administrador de paquetes Node.js.
Visual Studio
Visual Studio Code
Visual Studio para Mac
En el Explorador de soluciones, haga clic con el botón derecho en el proyecto y seleccione Agregar >
Client-Side Library (Biblioteca del lado cliente).
En el cuadro de diálogo Add Client-Side Library (Agregar biblioteca del lado cliente), en Proveedor,
seleccione unpkg.
En Biblioteca, escriba _@aspnet/signalr@1_ y seleccione la versión más reciente que no sea una versión
preliminar.

Seleccione Choose specific files (Elegir archivos específicos), expanda la carpeta dist/browser y seleccione
signalr.js y signalr.min.js.
Establezca Ubicación de destino en wwwroot/lib/signalr/ y seleccione Instalar.
LibMan crea una carpeta wwwroot/lib/signalr y copia en ella los archivos seleccionados.

Crear el concentrador SignalR


Un concentrador es una clase que actúa como una canalización general que controla la comunicación entre el
cliente y el servidor.
En la carpeta del proyecto SignalRChat, cree una carpeta Hubs.
En la carpeta Hubs, cree un archivo ChatHub.cs con el código siguiente:

using Microsoft.AspNetCore.SignalR;
using System.Threading.Tasks;

namespace SignalRChat.Hubs
{
public class ChatHub : Hub
{
public async Task SendMessage(string user, string message)
{
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
}
}

La clase ChatHub hereda de la clase Hub de SignalR. La clase Hub administra las conexiones, los grupos y la
mensajería.
Cualquier cliente conectado puede llamar al método SendMessage . Envía el mensaje recibido a todos los
clientes. El código de SignalR es asincrónico para proporcionar la máxima escalabilidad.

Configurar el proyecto para que use SignalR


El servidor de SignalR se debe configurar para que pase las solicitudes de SignalR a SignalR.
Agregue el código resaltado siguiente al archivo Startup.cs.
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
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; }

// 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.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

services.AddSignalR();
}

// 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.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseSignalR(routes =>
{
routes.MapHub<ChatHub>("/chatHub");
});
app.UseMvc();
}
}
}

Estos cambios agregan SignalR al sistema de inserción de dependencias y a la canalización de software


intermedio.
Crear el código de cliente de SignalR
Reemplace el contenido de Pages/Index.cshtml con el código siguiente:

@page
<div class="container">
<div class="row">&nbsp;</div>
<div class="row">
<div class="col-6">&nbsp;</div>
<div class="col-6">
User..........<input type="text" id="userInput" />
<br />
Message...<input type="text" id="messageInput" />
<input type="button" id="sendButton" value="Send Message" />
</div>
</div>
<div class="row">
<div class="col-12">
<hr />
</div>
</div>
<div class="row">
<div class="col-6">&nbsp;</div>
<div class="col-6">
<ul id="messagesList"></ul>
</div>
</div>
</div>
<script src="~/lib/signalr/dist/browser/signalr.js"></script>
<script src="~/js/chat.js"></script>

El código anterior:
Crea cuadros de texto para el nombre y el mensaje de texto, y un botón de envío.
Crea una lista con id="messagesList" para mostrar los mensajes que se reciben desde el concentrador
SignalR.
Incluye las referencias de script en SignalR y el código de aplicación de chat.js que se va a crear en el
paso siguiente.
En la carpeta wwwroot/js, cree un archivo chat.js con el código siguiente:
"use strict";

var connection = new signalR.HubConnectionBuilder().withUrl("/chatHub").build();

connection.on("ReceiveMessage", function (user, message) {


var msg = message.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
var encodedMsg = user + " says " + msg;
var li = document.createElement("li");
li.textContent = encodedMsg;
document.getElementById("messagesList").appendChild(li);
});

connection.start().catch(function (err) {
return console.error(err.toString());
});

document.getElementById("sendButton").addEventListener("click", function (event) {


var user = document.getElementById("userInput").value;
var message = document.getElementById("messageInput").value;
connection.invoke("SendMessage", user, message).catch(function (err) {
return console.error(err.toString());
});
event.preventDefault();
});

El código anterior:
Crea e inicia una conexión.
Agrega al botón de envío un controlador que envía mensajes al concentrador.
Agrega al objeto de conexión un controlador que recibe mensajes desde el concentrador y los agrega a la
lista.

Ejecutar la aplicación
Visual Studio
Visual Studio Code
Visual Studio para Mac
Presione CTRL+F5 para ejecutar la aplicación sin depurar.
Copie la dirección URL de la barra de direcciones, abra otra instancia o pestaña del explorador, y pegue la
dirección URL en la barra de direcciones.
Elija cualquier explorador, escriba un nombre y un mensaje, y haga clic en el botón Enviar.
El nombre y el mensaje se muestran en ambas páginas al instante.
TIP
Si la aplicación no funciona, abra las herramientas para desarrolladores del explorador (F12) y vaya a la consola. Es posible que
vea errores relacionados con el código HTML y JavaScript. Por ejemplo, suponga que coloca signalr.js en una carpeta distinta
a la indicada. En ese caso, la referencia a ese archivo no funcionará y verá un error 404 en la consola.

Pasos siguientes
Si quiere que los clientes se conecten a una aplicación de SignalR desde otros dominios, tendrá que habilitar el uso
compartido de recursos entre orígenes (CORS ). Para obtener más información, vea Uso compartido de recursos
entre orígenes.
Para obtener más información sobre SignalR, los concentradores y los clientes de JavaScript, vea estos recursos:
Introducción a SignalR para ASP.NET Core
Uso de concentradores de SignalR para ASP.NET Core
Cliente de JavaScript de SignalR para ASP.NET Core
Uso de SignalR de ASP.NET Core con TypeScript y
Webpack
23/07/2018 • 20 minutes to read • Edit Online

Por Sébastien Sougnez y Scott Addie


Webpack permite a los desarrolladores agrupar y compilar los recursos del lado cliente de una aplicación web. En
este tutorial se describe el uso de Webpack en una aplicación web de SignalR de ASP.NET Core cuyo cliente está
escrito en TypeScript.
En este tutorial aprenderá a:
Aplicación de scaffolding a una aplicación de inicio de SignalR de ASP.NET Core
Configuración del cliente TypeScript de SignalR
Configuración de una canalización de compilación mediante Webpack
Configuración del servidor de SignalR
Habilitar la comunicación entre cliente y servidor
Vea o descargue el código de ejemplo (cómo descargarlo)

Requisitos previos
Instale el software siguiente:
Visual Studio
CLI de .NET Core
.NET Core SDK 2.1 o posterior
Node.js con npm
Visual Studio 2017 versión 15.7.3 o posterior con la carga de trabajo ASP.NET y desarrollo web

Creación de la aplicación web ASP.NET Core


Visual Studio
CLI de .NET Core
Configure Visual Studio para buscar npm en la variable de entorno PATH. De forma predeterminada, Visual Studio
usa la versión de npm que se encuentra en su directorio de instalación. Siga estas instrucciones en Visual Studio:
1. Vaya a Herramientas > Opciones > Proyectos y soluciones > Administración de paquetes web >
Herramientas web externas.
2. Seleccione la entrada $ (PATH ) en la lista. Haga clic en la flecha arriba para mover la entrada a la segunda
posición de la lista. Por otro lado, la primera entrada hace referencia a los paquetes del proyecto local.
Se ha completado la configuración de Visual Studio. Es el momento de crear el proyecto.
1. Use la opción de menú Archivo > Nuevo > Proyecto y seleccione la plantilla Aplicación web ASP.NET
Core.
2. Asigne el nombre SignalRWebPack al proyecto y haga clic en el botón Aceptar.
3. Seleccione .NET Core en la lista desplegable de plataforma de destino y ASP.NET Core 2.1 en la lista
desplegable del selector de plataforma. Seleccione la plantilla Vacía y haga clic en el botón Aceptar.

Configuración de Webpack y TypeScript


Los pasos siguientes permiten configurar la conversión de TypeScript a JavaScript y la agrupación de los recursos
del lado cliente.
1. Ejecute el comando siguiente en la raíz del proyecto para crear un archivo package.json:

npm init -y

2. Agregue la propiedad resaltada al archivo package.json:

{
"name": "SignalRWebPack",
"version": "1.0.0",
"private": true,
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}

Si establece la propiedad private en true , evitará las advertencias de la instalación de paquetes en el paso
siguiente.
3. Instale los paquetes npm necesarios. Ejecute el comando siguiente desde la raíz del proyecto:
npm install -D -E clean-webpack-plugin@0.1.19 css-loader@0.28.11 html-webpack-plugin@3.2.0 mini-css-
extract-plugin@0.4.0 ts-loader@4.4.1 typescript@2.9.2 webpack@4.12.0 webpack-cli@3.0.6

Algunos detalles del comando para tener en cuenta:


En cada nombre de paquete, un número de versión sigue al signo @ . npm instala esas versiones de
paquete específicas.
La opción -E deshabilita el comportamiento predeterminado de npm de escribir operadores de
intervalo de versionamiento semántico en package.json. Por ejemplo, se usa "webpack": "4.12.0" en
lugar de "webpack": "^4.12.0" . Esta opción impide actualizaciones no deseadas a versiones más
recientes del paquete.
Vea la documentación oficial de npm-install para obtener más detalles.
4. Reemplace la propiedad scripts del archivo package.json por el fragmento de código siguiente:

"scripts": {
"build": "webpack --mode=development --watch",
"release": "webpack --mode=production",
"publish": "npm run release && dotnet publish -c Release"
},

Más detalles sobre los scripts:


build : agrupa los recursos del lado cliente en modo de desarrollo y supervisa los cambios del archivo. El
monitor de archivos hace que la agrupación se vuelva a generar cada vez que cambia un archivo del
proyecto. La opción mode deshabilita las optimizaciones de producción, como la agitación del árbol y la
minificación. Use build únicamente durante el desarrollo.
release : agrupa los recursos del lado cliente en modo de producción.
publish : ejecuta el script release para agrupar los recursos del lado cliente en modo de producción.
Llama al comando publish de la CLI de .NET Core para publicar la aplicación.
5. Cree un archivo denominado webpack.config.js, en la raíz del proyecto, con el contenido siguiente:
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const CleanWebpackPlugin = require("clean-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

module.exports = {
entry: "./src/index.ts",
output: {
path: path.resolve(__dirname, "wwwroot"),
filename: "[name].[chunkhash].js",
publicPath: "/"
},
resolve: {
extensions: [".js", ".ts"]
},
module: {
rules: [
{
test: /\.ts$/,
use: "ts-loader"
},
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, "css-loader"]
}
]
},
plugins: [
new CleanWebpackPlugin(["wwwroot/*"]),
new HtmlWebpackPlugin({
template: "./src/index.html"
}),
new MiniCssExtractPlugin({
filename: "css/[name].[chunkhash].css"
})
]
};

El archivo anterior configura la compilación de Webpack. Algunos detalles de configuración para tener en
cuenta:
La propiedad output invalida el valor predeterminado de dist. En su lugar, la agrupación se genera en el
directorio wwwroot.
La matriz resolve.extensions incluye .js para importar el código JavaScript cliente de SignalR.
6. Cree un directorio src en la raíz del proyecto. Su función es almacenar los activos del lado cliente del
proyecto.
7. Cree src/index.html con el contenido siguiente.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>ASP.NET Core SignalR</title>
</head>
<body>
<div id="divMessages" class="messages">
</div>
<div class="input-zone">
<label id="lblMessage" for="tbMessage">Message:</label>
<input id="tbMessage" class="input-zone-input" type="text" />
<button id="btnSend">Send</button>
</div>
</body>
</html>

El código HTML anterior define el marcado reutilizable de la página principal.


8. Cree un directorio src/css. Su objetivo es almacenar los archivos .css del proyecto.
9. Cree src/css/main.css con el contenido siguiente:

*, *::before, *::after {
box-sizing: border-box;
}

html, body {
margin: 0;
padding: 0;
}

.input-zone {
align-items: center;
display: flex;
flex-direction: row;
margin: 10px;
}

.input-zone-input {
flex: 1;
margin-right: 10px;
}

.message-author {
font-weight: bold;
}

.messages {
border: 1px solid #000;
margin: 10px;
max-height: 300px;
min-height: 300px;
overflow-y: auto;
padding: 5px;
}

El archivo main.css anterior aplica estilo a la aplicación.


10. Cree src/tsconfig.json con el contenido siguiente:
{
"compilerOptions": {
"target": "es5"
}
}

El código anterior configura el compilador de TypeScript para generar JavaScript compatible con
ECMAScript 5.
11. Cree src/index.ts con el contenido siguiente:

import "./css/main.css";

const divMessages: HTMLDivElement = document.querySelector("#divMessages");


const tbMessage: HTMLInputElement = document.querySelector("#tbMessage");
const btnSend: HTMLButtonElement = document.querySelector("#btnSend");
const username = new Date().getTime();

tbMessage.addEventListener("keyup", (e: KeyboardEvent) => {


if (e.keyCode === 13) {
send();
}
});

btnSend.addEventListener("click", send);

function send() {
}

El elemento TypeScript anterior recupera las referencias a elementos DOM y adjunta dos controladores de
eventos:
keyup : este evento se desencadena cuando el usuario escribe algo en el cuadro de texto identificado
como tbMessage . La función send se llama cuando el usuario presiona la tecla Entrar.
click : este evento se desencadena cuando el usuario clic en el botón Enviar. Se llama a la función send
.

Configuración de la aplicación ASP.NET Core


1. El código proporcionado en el método Startup.Configure muestra Hello World!. Reemplace la llamada al
método app.Run por las llamadas a UseDefaultFiles y UseStaticFiles.

app.UseDefaultFiles();
app.UseStaticFiles();

El código anterior permite que el servidor busque y proporcione el archivo index.html, con independencia de
que el usuario escriba su dirección URL completa o la dirección URL raíz de la aplicación web.
2. Llame a AddSignalR en el método Startup.ConfigureServices . Esto permite agregar los servicios SignalR al
proyecto.

services.AddSignalR();

3. Asigne una ruta /hub al concentrador ChatHub . Agregue las líneas siguientes al final del método
Startup.Configure :
app.UseSignalR(options =>
{
options.MapHub<ChatHub>("/hub");
});

4. Cree un directorio denominado Hubs en la raíz del proyecto. Su objetivo es almacenar el concentrador de
SignalR, que se crea en el paso siguiente.
5. Cree el concentrador Hubs/ChatHub.cs con el código siguiente:

using Microsoft.AspNetCore.SignalR;
using System.Threading.Tasks;

namespace SignalRWebPack.Hubs
{
public class ChatHub : Hub
{
}
}

6. Agregue el código siguiente en la parte superior del archivo Startup.cs para resolver la referencia a ChatHub :

using SignalRWebPack.Hubs;

Habilitar la comunicación entre cliente y servidor


Actualmente, en la aplicación se muestra un formulario simple para enviar mensajes. Al intentar hacer algo no
sucede nada. El servidor está escuchando en una ruta específica, pero no hace nada con los mensajes enviados.
1. Ejecute el comando siguiente en la raíz del proyecto:

npm install @aspnet/signalr

El comando anterior instala el cliente TypeScript de SignalR, que permite al cliente enviar mensajes al
servidor.
2. Agregue el código resaltado al archivo src/index.ts:
import "./css/main.css";
import * as signalR from "@aspnet/signalr";

const divMessages: HTMLDivElement = document.querySelector("#divMessages");


const tbMessage: HTMLInputElement = document.querySelector("#tbMessage");
const btnSend: HTMLButtonElement = document.querySelector("#btnSend");
const username = new Date().getTime();

const connection = new signalR.HubConnectionBuilder()


.withUrl("/hub")
.build();

connection.start().catch(err => document.write(err));

connection.on("messageReceived", (username: string, message: string) => {


let m = document.createElement("div");

m.innerHTML =
`<div class="message__author">${username}</div><div>${message}</div>`;

divMessages.appendChild(m);
divMessages.scrollTop = divMessages.scrollHeight;
});

tbMessage.addEventListener("keyup", (e: KeyboardEvent) => {


if (e.keyCode === 13) {
send();
}
});

btnSend.addEventListener("click", send);

function send() {
}

El código anterior admite la recepción de mensajes desde el servidor. La clase HubConnectionBuilder crea un
generador para configurar la conexión al servidor. La función withUrl configura la dirección URL del
concentrador.
SignalR permite el intercambio de mensajes entre un cliente y un servidor. Cada mensaje tiene un nombre
específico. Por ejemplo, puede haber mensajes con el nombre messageReceived que ejecuten la lógica
responsable de mostrar el mensaje nuevo en la zona de mensajes. La escucha a un mensaje concreto se
puede realizar mediante la función on . Puede escuchar a cualquier número de nombres de mensaje.
También se pueden pasar parámetros al mensaje, como el nombre del autor y el contenido del mensaje
recibido. Una vez que el cliente recibe un mensaje, se crea un elemento div con el nombre del autor y el
contenido del mensaje en su atributo innerHTML . Se agrega al elemento div principal que muestra los
mensajes.
3. Ahora que el cliente puede recibir mensajes, debe configurarlo para poder enviarlos. Agregue el código
resaltado al archivo src/index.ts:
import "./css/main.css";
import * as signalR from "@aspnet/signalr";

const divMessages: HTMLDivElement = document.querySelector("#divMessages");


const tbMessage: HTMLInputElement = document.querySelector("#tbMessage");
const btnSend: HTMLButtonElement = document.querySelector("#btnSend");
const username = new Date().getTime();

const connection = new signalR.HubConnectionBuilder()


.withUrl("/hub")
.build();

connection.start().catch(err => document.write(err));

connection.on("messageReceived", (username: string, message: string) => {


let messageContainer = document.createElement("div");

messageContainer.innerHTML =
`<div class="message-author">${username}</div><div>${message}</div>`;

divMessages.appendChild(messageContainer);
divMessages.scrollTop = divMessages.scrollHeight;
});

tbMessage.addEventListener("keyup", (e: KeyboardEvent) => {


if (e.keyCode === 13) {
send();
}
});

btnSend.addEventListener("click", send);

function send() {
connection.send("newMessage", username, tbMessage.value)
.then(() => tbMessage.value = "");
}

El envío de mensajes a través de la conexión de WebSockets requiere llamar al método send . El primer
parámetro del método es el nombre del mensaje. Los datos del mensaje se encuentran en los otros
parámetros. En este ejemplo, se envía al servidor un mensaje identificado como newMessage . El mensaje está
formado por el nombre de usuario y la entrada del usuario desde un cuadro de texto. Si el envío funciona, se
borra el valor del cuadro de texto.
4. Agregue el método resaltado a la clase ChatHub :

using Microsoft.AspNetCore.SignalR;
using System.Threading.Tasks;

namespace SignalRWebPack.Hubs
{
public class ChatHub : Hub
{
public async Task NewMessage(string username, string message)
{
await Clients.All.SendAsync("messageReceived", username, message);
}
}
}

El código anterior difunde los mensajes recibidos a todos los usuarios conectados, una vez que el servidor
los recibe. No es necesario tener un método on genérico para recibir todos los mensajes. Basta con un
método que tenga el nombre del mensaje.
En este ejemplo, el cliente de TypeScript envía un mensaje que se identifica como newMessage . El método
NewMessage de C# espera los datos enviados por el cliente. Se realiza una llamada al método SendAsync de
Clients.All. Los mensajes recibidos se envían a todos los clientes conectados al concentrador.

Prueba de la aplicación
Confirme que la aplicación funciona con los pasos siguientes.
Visual Studio
CLI de .NET Core
1. Ejecute Webpack en modo release. Desde la ventana Consola del Administrador de paquetes, ejecute el
comando siguiente en la raíz del proyecto:

npm run release

Este comando da como resultado la entrega de los activos del lado cliente cuando se ejecuta la aplicación.
Los recursos se colocan en la carpeta wwwroot.
Webpack ha completado las tareas siguientes:
Purgar el contenido del directorio wwwroot.
Convertir TypeScript en JavaScript, proceso conocido como transpilación.
Alterar el código JavaScript generado para reducir el tamaño del archivo, proceso conocido como
minificación.
Copiar los archivos JavaScript, CSS y HTML procesados desde src en el directorio wwwroot.
Insertar los elementos siguientes en el archivo wwwroot/index.html:
Etiqueta <link> , que hace referencia al archivo wwwroot/main.<hash>.css. Esta etiqueta se coloca
inmediatamente antes de la etiqueta </head> de cierre.
Etiqueta <script> , que hace referencia al archivo wwwroot/main.<hash>.js minificado. Esta
etiqueta se coloca inmediatamente antes de la etiqueta </body> de cierre.
2. Seleccione Depurar > Iniciar sin depurar para iniciar la aplicación en un explorador sin adjuntar el
depurador. El archivo wwwroot/index.html se entrega en http://localhost:<port_number> .
3. Abra otra instancia del explorador (sirve cualquiera). Pegue la dirección URL en la barra de direcciones.
4. Elija un explorador, escriba algo en el cuadro de texto Mensaje y haga clic en el botón Enviar. El nombre de
usuario único y el mensaje se muestran en las dos páginas al instante.
Recursos adicionales
Cliente ASP.NET Core SignalR JavaScript
Usar concentradores en ASP.NET Core SignalR
Crear una aplicación web con ASP.NET Core MVC en
Windows con Visual Studio
11/07/2018 • 2 minutes to read • Edit Online

En este tutorial se muestra el desarrollo web de ASP.NET Core MVC con controladores y vistas. Razor Pages es
una característica del marco de ASP.NET Core MVC que facilita y rentabiliza la compilación y las pruebas en la
interfaz de usuario web. Puede usar Razor Pages con controladores y vistas en el mismo proyecto.
Se recomienda probar el tutorial de Razor Pages antes que la versión de MVC, controladores o vistas. 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: Creación de una aplicación de ASP.NET Core MVC con Visual Studio Code
La serie del tutorial 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
24/09/2018 • 11 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. Razor Pages es
una característica del marco de ASP.NET Core MVC que facilita y rentabiliza la compilación y las pruebas en la
interfaz de usuario web. Puede usar Razor Pages con controladores y vistas en el mismo proyecto.
Se recomienda probar el tutorial de Razor Pages antes que la versión de MVC, controladores o vistas. 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


Visual Studio 2017 versión 15.7.3 o posterior con las cargas de trabajo siguientes:
Desarrollo de ASP.NET y web
Desarrollo multiplataforma de .NET Core
SDK de .NET Core 2.1 o versiones posteriores

Creación de 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.

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.1.
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 básico, 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.
ASP.NET Core 2.x
ASP.NET Core 1.x
Instale uno de los siguientes:
Herramientas de CLI: Windows, Linux o macOS: .NET Core SDK 2.0 o posterior
Herramientas de IDE/editor
Windows: Visual Studio para Windows
Carga de trabajo de ASP.NET y desarrollo web
Carga de trabajo Desarrollo multiplataforma de .NET Core
Linux: Visual Studio Code
macOS: Visual Studio para 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 básico, 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
16/07/2018 • 12 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
Agregar una vista a una aplicación de ASP.NET Core
MVC
04/07/2018 • 16 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í:

@inject Microsoft.ApplicationInsights.AspNetCore.JavaScriptSnippet JavaScriptSnippet


<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - Movie App</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" />
<link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" />
</environment>
@Html.Raw(JavaScriptSnippet.FullScript)
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<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="Movies" asp-action="Index" class="navbar-brand">Movie App</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>
</div>
</div>
</nav>
<div class="container body-content">
@RenderBody()
<hr />
<footer>
<p>&copy; 2017 - MvcMovie</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>

@inject Microsoft.ApplicationInsights.AspNetCore.JavaScriptSnippet JavaScriptSnippet


<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - Movie App</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-
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>
@Html.Raw(JavaScriptSnippet.FullScript)
</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="Movies" asp-action="Index" class="navbar-brand">Movie App</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>
</div>
</div>
</nav>
<div class="container body-content">
@RenderBody()
<hr />
<footer>
<p>&copy; 2017 - MvcMovie</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>

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
24/09/2018 • 19 minutes to read • Edit Online

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 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 >
Nuevo elemento con scaffold.
En el cuadro de diálogo Agregar scaffold, pulse en Controlador de MVC con vistas que usan Entity
Framework > Agregar.

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:

Add-Migration Initial
Update-Database

Pase por alto el siguiente mensaje de error, lo subsanaremos en el próximo tutorial:


Microsoft.EntityFrameworkCore.Model.Validation[30000 ]
No type was specified for the decimal column 'Price' on entity type 'Movie'. This will cause values to be silently
truncated if they do not fit in the default precision and scale. Explicitly specify the SQL server column type that can
accommodate all the values using 'ForHasColumnType()'. (No se ha especificado ningún tipo en la columna
decimal "Price" en el tipo de entidad "Movie". Especifique expresamente el tipo de columna de SQL Server que
tenga cabida para todos los valores usando "ForHasColumnType()". Esto hará que los valores se trunquen
inadvertidamente si no caben según la precisión y escala predeterminados.)
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 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. Para más información, vea https://github.com/aspnet/Docs/issues/4076 y Recursos
adicionales. Por ahora, escriba solamente 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)


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

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

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


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

return View(movie);
}
// 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 FirstOrDefaultAsync 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


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

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 los asistentes
de 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
Asistentes 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
24/09/2018 • 5 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)


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

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

public void ConfigureServices(IServiceCollection services)


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

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

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
Reemplace el contenido de Program.cs por el código siguiente:

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

namespace MvcMovie
{
public class Program
{
public static void Main(string[] args)
{
var host = CreateWebHostBuilder(args).Build();

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


{
var services = scope.ServiceProvider;

try
{
var context = services.GetRequiredService<MvcMovieContext>();
context.Database.Migrate();
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 IWebHostBuilder CreateWebHostBuilder(string[] args) =>


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

ASP.NET Core 2.x


ASP.NET Core 1.x
Agregue el inicializador al método Main del archivo Program.cs:
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using MvcMovie.Models;
using System;

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

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


{
var services = scope.ServiceProvider;

try
{
// Requires using MvcMovie.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.

A N T E R IO R S IG U IE N T E
Vistas y métodos de controlador en ASP.NET Core
24/09/2018 • 16 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;
using System.ComponentModel.DataAnnotations.Schema;

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

[Column(TypeName = "decimal(18, 2)")]


public decimal Price { get; set; }
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

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

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.
La anotación de datos [Column(TypeName = "decimal(18, 2)")] es necesaria para que Entity Framework Core
asigne correctamente Price a la moneda en la base de datos. Para más información, vea Tipos de datos.
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 el asistente 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>

Los asistentes 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).
Los asistentes 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.FindAsync(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);
}

// 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] .
[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(nameof(Index));
}
return View(movie);
}
// 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 el asistente de etiquetas de formulario.

<form asp-action="Edit">

El asistente 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.FindAsync(id);


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

// 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 del asistente de etiquetas para simplificar el marcado HTML. El
asistente de etiquetas muestra el nombre del campo: "Title" (Título), "ReleaseDate" (Fecha de lanzamiento),
"Genre" (Género) o "Price" (Precio). El asistente de etiquetas de entrada representa un elemento HTML <input> .
El asistente 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 el asistente 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 .
[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(nameof(Index));
}
return View(movie);
}
// 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 el asistente 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. El asistente 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 los asistentes de etiquetas
Creación de asistentes de etiquetas
Prevención de ataques de falsificación de solicitudes
Protección del controlador frente al exceso de publicación
ViewModels
Asistente de etiquetas de formulario
Asistente de etiquetas de entrada
Asistente de etiquetas de elementos de etiqueta
Asistente de etiquetas de selección
Asistente de etiquetas de validación

A N T E R IO R S IG U IE N T E
Agregar búsqueda a una aplicación de ASP.NET Core
MVC
16/07/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 campo nuevo a una aplicación de
ASP.NET Core MVC
24/09/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; }

[Column(TypeName = "decimal(18, 2)")]


public decimal Price { get; set; }
public string Rating { get; set; }
}

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


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 asistentes 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
Agregar validación a una aplicación ASP.NET Core
MVC
16/07/2018 • 19 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 Core MVC le anima a que
especifique la funcionalidad o el comportamiento una sola vez y a que luego los 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 .
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)]
[Column(TypeName = "decimal(18, 2)")]
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; }
}

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 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 solo pueden usar letras (no se permiten mayúsculas iniciales, 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] .

El que ASP.NET Core aplique automáticamente las reglas de validación ayuda a que su 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>

El asistente de etiquetas de entrada usa los atributos DataAnnotations y genera los atributos HTML necesarios
para la validación de jQuery en el lado cliente. El asistente 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 con el guion) 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:

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


[Column(TypeName = "decimal(18, 2)")]
public decimal Price { get; set; }

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

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

[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-]*$"), Required, StringLength(5)]


public string Rating { get; set; }
}

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 los asistentes de etiquetas
Creación de asistentes 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
24/09/2018 • 6 minutes to read • Edit Online

Por Rick Anderson


Abra el controlador Movie y examine el método Details :

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

var movie = await _context.Movie


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

return View(movie);
}

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

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

var movie = await _context.Movie


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

return View(movie);
}

// POST: Movies/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
var movie = await _context.Movie.FindAsync(id);
_context.Movie.Remove(movie);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
// GET: Movies/Delete/5
public async Task<IActionResult> Delete(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);
}

// POST: Movies/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
var movie = await _context.Movie.SingleOrDefaultAsync(m => m.ID == id);
_context.Movie.Remove(movie);
await _context.SaveChangesAsync();
return RedirectToAction("Index");
}

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
24/08/2018 • 8 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:
[Produces("application/json")]
[Route("api/[controller]")]
public class PetsController : ControllerBase
{
private readonly PetsRepository _repository;

public PetsController(PetsRepository repository)


{
_repository = repository;
}

[HttpGet]
public async Task<ActionResult<List<Pet>>> GetAllAsync()
{
return await _repository.GetPetsAsync();
}

[HttpGet("{id}")]
[ProducesResponseType(404)]
public async Task<ActionResult<Pet>> GetByIdAsync(int id)
{
var pet = await _repository.GetPetAsync(id);

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

return pet;
}

[HttpPost]
[ProducesResponseType(400)]
public async Task<ActionResult<Pet>> CreateAsync(Pet pet)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}

await _repository.AddPetAsync(pet);

return CreatedAtAction(nameof(GetByIdAsync),
new { id = pet.Id }, pet);
}
}
[Produces("application/json")]
[Route("api/[controller]")]
public class PetsController : ControllerBase
{
private readonly PetsRepository _repository;

public PetsController(PetsRepository repository)


{
_repository = repository;
}

[HttpGet]
[ProducesResponseType(typeof(IEnumerable<Pet>), 200)]
public async Task<IActionResult> GetAllAsync()
{
var pets = await _repository.GetPetsAsync();

return Ok(pets);
}

[HttpGet("{id}")]
[ProducesResponseType(typeof(Pet), 200)]
[ProducesResponseType(404)]
public async Task<IActionResult> GetByIdAsync(int id)
{
var pet = await _repository.GetPetAsync(id);

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

return Ok(pet);
}

[HttpPost]
[ProducesResponseType(typeof(Pet), 201)]
[ProducesResponseType(400)]
public async Task<IActionResult> CreateAsync([FromBody] Pet pet)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}

await _repository.AddPetAsync(pet);

return CreatedAtAction(nameof(GetByIdAsync),
new { id = pet.Id }, pet);
}
}

La clase ControllerBase proporciona acceso a varios métodos y propiedades. En el código anterior, algunos
ejemplos son BadRequest(ModelStateDictionary) y CreatedAtAction(String, Object, Object). Se llama a estos
métodos 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 controlar 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:
[Route("api/[controller]")]
[ApiController]
public class ProductsController : ControllerBase

Para usar este atributo se requiere una versión de compatibilidad de 2.1 o posterior, establecida mediante
SetCompatibilityVersion. Por ejemplo, el código resaltado en Startup.ConfigureServices establece la marca de
compatibilidad 2.1:

services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

Para obtener más información, vea Versión de compatibilidad para ASP.NET Core MVC.
El atributo [ApiController] se suele emparejar con ControllerBase para habilitar el comportamiento específico de
REST para los controladores. 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] :

[ApiController]
public class MyBaseController
{
}

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:

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

El comportamiento predeterminado se deshabilita al establecer la propiedad SuppressModelStateInvalidFilter en


true . Agregue el siguiente código en Startup.ConfigureServices después de
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); :

services.Configure<ApiBehaviorOptions>(options =>
{
options.SuppressConsumesConstraintForFormFileParameters = true;
options.SuppressInferBindingSourcesForParameters = true;
options.SuppressModelStateInvalidFilter = true;
});

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


ATRIBUTO ORIGEN DE ENLACE

[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

WARNING
No use [FromRoute] si los valores pueden contener %2f (es decir, / ). %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:

[HttpGet]
public async Task<ActionResult<List<Product>>> GetAsync(
[FromQuery] bool discontinuedOnly = false)
{
List<Product> products = null;

if (discontinuedOnly)
{
products = await _repository.GetDiscontinuedProductsAsync();
}
else
{
products = await _repository.GetProductsAsync();
}

return products;
}

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 y CancellationToken. El código de
inferencia del origen de enlace omite esos tipos especiales. En el caso de los tipos simples, como string y int ,
[FromBody] no se infiere. Así pues, para los tipos simples, en los casos en los que quiera utilizar dicha
funcionalidad, conviene usar el atributo [FromBody] . 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:
// Don't do this. All of the following actions result in an exception.
[HttpPost]
public IActionResult Action1(Product product,
Order order) => null;

[HttpPost]
public IActionResult Action2(Product product,
[FromBody] Order order) => null;

[HttpPost]
public IActionResult Action3([FromBody] Product product,
[FromBody] Order order) => null;

[FromForm ] se infiere para los parámetros de acción de tipo IFormFile y 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 al establecer la propiedad


SuppressInferBindingSourcesForParameters en true . Agregue el siguiente código en Startup.ConfigureServices
después de services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); :

services.Configure<ApiBehaviorOptions>(options =>
{
options.SuppressConsumesConstraintForFormFileParameters = true;
options.SuppressInferBindingSourcesForParameters = true;
options.SuppressModelStateInvalidFilter = true;
});

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 al establecer la propiedad


SuppressConsumesConstraintForFormFileParameters en true . Agregue el siguiente código en
Startup.ConfigureServices después de
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); :

services.Configure<ApiBehaviorOptions>(options =>
{
options.SuppressConsumesConstraintForFormFileParameters = true;
options.SuppressInferBindingSourcesForParameters = true;
options.SuppressModelStateInvalidFilter = true;
});

Requisito de enrutamiento mediante atributos


El enrutamiento mediante atributos pasa a ser un requisito. Por ejemplo:

[Route("api/[controller]")]
[ApiController]
public class ProductsController : ControllerBase

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 de ASP.NET Core Web API
Formateadores personalizados en ASP.NET Core Web API
Aplicación de formato a datos de respuesta en ASP.NET Core Web API
Páginas de ayuda de ASP.NET Core Web API con Swagger/Open API
Enrutar a acciones de controlador de ASP.NET Core
Crear una API web con ASP.NET Core y Visual Studio
Code
06/08/2018 • 28 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 CLR 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
Instale el software siguiente:
.NET Core SDK 2.0 o posterior
Visual Studio Code
C# para Visual Studio Code
SDK de .NET Core 2.1 o versiones posteriores
Visual Studio Code
C# para 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.1 o posterior, se agrega la referencia de paquete
Microsoft.AspNetCore.App al archivo TodoApi.csproj. Agregue el atributo Version , si aún no se ha especificado.

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.4" />
</ItemGroup>

Al crear un proyecto en ASP.NET Core 2.0, se agrega la referencia de paquete Microsoft.AspNetCore.All al archivo
TodoApi.csproj:

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.1.4" />
</ItemGroup>

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:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using TodoApi.Models;

namespace TodoApi
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<TodoContext>(opt =>
opt.UseInMemoryDatabase("TodoList"));
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}

public void Configure(IApplicationBuilder app)


{
app.UseMvc();
}
}
}

using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using TodoApi.Models;

namespace TodoApi
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<TodoContext>(opt =>
opt.UseInMemoryDatabase("TodoList"));
services.AddMvc();
}

public void Configure(IApplicationBuilder app)


{
app.UseMvc();
}
}
}

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:
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Linq;
using TodoApi.Models;

namespace TodoApi.Controllers
{
[Route("api/[controller]")]
public class TodoController : ControllerBase
{
private readonly TodoContext _context;

public TodoController(TodoContext context)


{
_context = context;

if (_context.TodoItems.Count() == 0)
{
_context.TodoItems.Add(new TodoItem { Name = "Item1" });
_context.SaveChanges();
}
}
}
}

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.

using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Linq;
using TodoApi.Models;

namespace TodoApi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class TodoController : ControllerBase
{
private readonly TodoContext _context;

public TodoController(TodoContext context)


{
_context = context;

if (_context.TodoItems.Count() == 0)
{
// Create a new TodoItem if collection is empty,
// which means you can't delete all TodoItems.
_context.TodoItems.Add(new TodoItem { Name = "Item1" });
_context.SaveChanges();
}
}
}
}

El código anterior:
Define una clase de controlador de API sin métodos.
Crea una tarea pendiente cuando TodoItems está vacío. No podrá eliminar todas las tareas pendientes porque el
constructor crea una si TodoItems está vacío.
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 :

[HttpGet]
public List<TodoItem> GetAll()
{
return _context.TodoItems.ToList();
}

[HttpGet("{id}", Name = "GetTodo")]


public IActionResult GetById(long id)
{
var item = _context.TodoItems.Find(id);
if (item == null)
{
return NotFound();
}
return Ok(item);
}

[HttpGet]
public ActionResult<List<TodoItem>> GetAll()
{
return _context.TodoItems.ToList();
}

[HttpGet("{id}", Name = "GetTodo")]


public ActionResult<TodoItem> GetById(long id)
{
var item = _context.TodoItems.Find(id);
if (item == null)
{
return NotFound();
}
return item;
}

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:

namespace TodoApi.Controllers
{
[Route("api/[controller]")]
public class TodoController : ControllerBase
{
private readonly TodoContext _context;

namespace TodoApi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class TodoController : ControllerBase
{
private readonly TodoContext _context;

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.

[HttpGet("{id}", Name = "GetTodo")]


public IActionResult GetById(long id)
{
var item = _context.TodoItems.Find(id);
if (item == null)
{
return NotFound();
}
return Ok(item);
}

[HttpGet("{id}", Name = "GetTodo")]


public ActionResult<TodoItem> GetById(long id)
{
var item = _context.TodoItems.Find(id);
if (item == null)
{
return NotFound();
}
return item;
}

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');
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')[0].checked = 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 :

[HttpPost]
public IActionResult Create([FromBody] TodoItem item)
{
if (item == null)
{
return BadRequest();
}

_context.TodoItems.Add(item);
_context.SaveChanges();

return CreatedAtRoute("GetTodo", new { id = item.Id }, item);


}

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.

[HttpPost]
public IActionResult Create(TodoItem item)
{
_context.TodoItems.Add(item);
_context.SaveChanges();

return CreatedAtRoute("GetTodo", new { id = item.Id }, item);


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

[HttpGet("{id}", Name = "GetTodo")]


public IActionResult GetById(long id)
{
var item = _context.TodoItems.Find(id);
if (item == null)
{
return NotFound();
}
return Ok(item);
}

[HttpGet("{id}", Name = "GetTodo")]


public ActionResult<TodoItem> GetById(long id)
{
var item = _context.TodoItems.Find(id);
if (item == null)
{
return NotFound();
}
return item;
}

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 :

[HttpPut("{id}")]
public IActionResult Update(long id, [FromBody] TodoItem item)
{
if (item == null || item.Id != id)
{
return BadRequest();
}

var todo = _context.TodoItems.Find(id);


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

todo.IsComplete = item.IsComplete;
todo.Name = item.Name;

_context.TodoItems.Update(todo);
_context.SaveChanges();
return NoContent();
}
[HttpPut("{id}")]
public IActionResult Update(long id, TodoItem item)
{
var todo = _context.TodoItems.Find(id);
if (todo == null)
{
return NotFound();
}

todo.IsComplete = item.IsComplete;
todo.Name = item.Name;

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

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
24/09/2018 • 28 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 CLR 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 para 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:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using TodoApi.Models;

namespace TodoApi
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<TodoContext>(opt =>
opt.UseInMemoryDatabase("TodoList"));
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}

public void Configure(IApplicationBuilder app)


{
app.UseMvc();
}
}
}

using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using TodoApi.Models;

namespace TodoApi
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<TodoContext>(opt =>
opt.UseInMemoryDatabase("TodoList"));
services.AddMvc();
}

public void Configure(IApplicationBuilder app)


{
app.UseMvc();
}
}
}

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:
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Linq;
using TodoApi.Models;

namespace TodoApi.Controllers
{
[Route("api/[controller]")]
public class TodoController : ControllerBase
{
private readonly TodoContext _context;

public TodoController(TodoContext context)


{
_context = context;

if (_context.TodoItems.Count() == 0)
{
_context.TodoItems.Add(new TodoItem { Name = "Item1" });
_context.SaveChanges();
}
}
}
}

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.

using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Linq;
using TodoApi.Models;

namespace TodoApi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class TodoController : ControllerBase
{
private readonly TodoContext _context;

public TodoController(TodoContext context)


{
_context = context;

if (_context.TodoItems.Count() == 0)
{
// Create a new TodoItem if collection is empty,
// which means you can't delete all TodoItems.
_context.TodoItems.Add(new TodoItem { Name = "Item1" });
_context.SaveChanges();
}
}
}
}

El código anterior:
Define una clase de controlador de API sin métodos.
Crea una tarea pendiente cuando TodoItems está vacío. No podrá eliminar todas las tareas pendientes porque el
constructor crea una si TodoItems está vacío.
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 :

[HttpGet]
public List<TodoItem> GetAll()
{
return _context.TodoItems.ToList();
}

[HttpGet("{id}", Name = "GetTodo")]


public IActionResult GetById(long id)
{
var item = _context.TodoItems.Find(id);
if (item == null)
{
return NotFound();
}
return Ok(item);
}

[HttpGet]
public ActionResult<List<TodoItem>> GetAll()
{
return _context.TodoItems.ToList();
}

[HttpGet("{id}", Name = "GetTodo")]


public ActionResult<TodoItem> GetById(long id)
{
var item = _context.TodoItems.Find(id);
if (item == null)
{
return NotFound();
}
return item;
}

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:

namespace TodoApi.Controllers
{
[Route("api/[controller]")]
public class TodoController : ControllerBase
{
private readonly TodoContext _context;

namespace TodoApi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class TodoController : ControllerBase
{
private readonly TodoContext _context;

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.

[HttpGet("{id}", Name = "GetTodo")]


public IActionResult GetById(long id)
{
var item = _context.TodoItems.Find(id);
if (item == null)
{
return NotFound();
}
return Ok(item);
}

[HttpGet("{id}", Name = "GetTodo")]


public ActionResult<TodoItem> GetById(long id)
{
var item = _context.TodoItems.Find(id);
if (item == null)
{
return NotFound();
}
return item;
}

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
[HttpPost]
public IActionResult Create([FromBody] TodoItem item)
{
if (item == null)
{
return BadRequest();
}

_context.TodoItems.Add(item);
_context.SaveChanges();

return CreatedAtRoute("GetTodo", new { id = item.Id }, item);


}

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.

[HttpPost]
public IActionResult Create(TodoItem item)
{
_context.TodoItems.Add(item);
_context.SaveChanges();

return CreatedAtRoute("GetTodo", new { id = item.Id }, item);


}

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
[HttpPut("{id}")]
public IActionResult Update(long id, [FromBody] TodoItem item)
{
if (item == null || item.Id != id)
{
return BadRequest();
}

var todo = _context.TodoItems.Find(id);


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

todo.IsComplete = item.IsComplete;
todo.Name = item.Name;

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

[HttpPut("{id}")]
public IActionResult Update(long id, TodoItem item)
{
var todo = _context.TodoItems.Find(id);
if (todo == null)
{
return NotFound();
}

todo.IsComplete = item.IsComplete;
todo.Name = item.Name;

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

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');
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')[0].checked = 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
14/08/2018 • 28 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 en 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 CLR 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 2017 versión 15.7.3 o posterior con las cargas de trabajo siguientes:
Desarrollo de ASP.NET y web
Desarrollo multiplataforma de .NET Core
SDK de .NET Core 2.1 o versiones posteriores

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:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using TodoApi.Models;

namespace TodoApi
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<TodoContext>(opt =>
opt.UseInMemoryDatabase("TodoList"));
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}

public void Configure(IApplicationBuilder app)


{
app.UseMvc();
}
}
}

using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using TodoApi.Models;

namespace TodoApi
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<TodoContext>(opt =>
opt.UseInMemoryDatabase("TodoList"));
services.AddMvc();
}

public void Configure(IApplicationBuilder app)


{
app.UseMvc();
}
}
}

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:

using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Linq;
using TodoApi.Models;

namespace TodoApi.Controllers
{
[Route("api/[controller]")]
public class TodoController : ControllerBase
{
private readonly TodoContext _context;

public TodoController(TodoContext context)


{
_context = context;

if (_context.TodoItems.Count() == 0)
{
_context.TodoItems.Add(new TodoItem { Name = "Item1" });
_context.SaveChanges();
}
}
}
}

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.
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Linq;
using TodoApi.Models;

namespace TodoApi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class TodoController : ControllerBase
{
private readonly TodoContext _context;

public TodoController(TodoContext context)


{
_context = context;

if (_context.TodoItems.Count() == 0)
{
// Create a new TodoItem if collection is empty,
// which means you can't delete all TodoItems.
_context.TodoItems.Add(new TodoItem { Name = "Item1" });
_context.SaveChanges();
}
}
}
}

El código anterior:
Define una clase de controlador de API sin métodos.
Crea una tarea pendiente cuando TodoItems está vacío. No podrá eliminar todas las tareas pendientes
porque el constructor crea una si TodoItems está vacío.
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 :
[HttpGet]
public List<TodoItem> GetAll()
{
return _context.TodoItems.ToList();
}

[HttpGet("{id}", Name = "GetTodo")]


public IActionResult GetById(long id)
{
var item = _context.TodoItems.Find(id);
if (item == null)
{
return NotFound();
}
return Ok(item);
}

[HttpGet]
public ActionResult<List<TodoItem>> GetAll()
{
return _context.TodoItems.ToList();
}

[HttpGet("{id}", Name = "GetTodo")]


public ActionResult<TodoItem> GetById(long id)
{
var item = _context.TodoItems.Find(id);
if (item == null)
{
return NotFound();
}
return item;
}

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:
namespace TodoApi.Controllers
{
[Route("api/[controller]")]
public class TodoController : ControllerBase
{
private readonly TodoContext _context;

namespace TodoApi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class TodoController : ControllerBase
{
private readonly TodoContext _context;

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.

[HttpGet("{id}", Name = "GetTodo")]


public IActionResult GetById(long id)
{
var item = _context.TodoItems.Find(id);
if (item == null)
{
return NotFound();
}
return Ok(item);
}

[HttpGet("{id}", Name = "GetTodo")]


public ActionResult<TodoItem> GetById(long id)
{
var item = _context.TodoItems.Find(id);
if (item == null)
{
return NotFound();
}
return item;
}

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 :

[HttpPost]
public IActionResult Create([FromBody] TodoItem item)
{
if (item == null)
{
return BadRequest();
}

_context.TodoItems.Add(item);
_context.SaveChanges();

return CreatedAtRoute("GetTodo", new { id = item.Id }, item);


}

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.

[HttpPost]
public IActionResult Create(TodoItem item)
{
_context.TodoItems.Add(item);
_context.SaveChanges();

return CreatedAtRoute("GetTodo", new { id = item.Id }, item);


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

[HttpGet("{id}", Name = "GetTodo")]


public IActionResult GetById(long id)
{
var item = _context.TodoItems.Find(id);
if (item == null)
{
return NotFound();
}
return Ok(item);
}

[HttpGet("{id}", Name = "GetTodo")]


public ActionResult<TodoItem> GetById(long id)
{
var item = _context.TodoItems.Find(id);
if (item == null)
{
return NotFound();
}
return item;
}

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 :

[HttpPut("{id}")]
public IActionResult Update(long id, [FromBody] TodoItem item)
{
if (item == null || item.Id != id)
{
return BadRequest();
}

var todo = _context.TodoItems.Find(id);


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

todo.IsComplete = item.IsComplete;
todo.Name = item.Name;

_context.TodoItems.Update(todo);
_context.SaveChanges();
return NoContent();
}
[HttpPut("{id}")]
public IActionResult Update(long id, TodoItem item)
{
var todo = _context.TodoItems.Find(id);
if (todo == null)
{
return NotFound();
}

todo.IsComplete = item.IsComplete;
todo.Name = item.Name;

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

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>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')[0].checked = 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
11/07/2018 • 2 minutes to read • Edit Online

En esta serie de tutoriales se explica cómo crear aplicaciones web de Razor Pages de ASP.NET Core que usen
Entity Framework (EF ) Core para acceder a los datos.
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
24/09/2018 • 29 minutes to read • Edit Online

La versión de ASP.NET Core 2.0 de este tutorial se puede encontrar en este archivo PDF.
La versión de ASP.NET Core 2.1 de este tutorial presenta muchas mejoras con respecto a la versión 2.0.
Por Tom Dykstra y Rick Anderson
En la aplicación web de ejemplo Contoso University se muestra cómo crear una aplicación web de Razor Pages de
ASP.NET Core con Entity Framework (EF ) Core.
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
Visual Studio
CLI de .NET Core
Visual Studio 2017 versión 15.7.3 o posterior con las cargas de trabajo siguientes:
Desarrollo de ASP.NET y web
Desarrollo multiplataforma de .NET Core
SDK de .NET Core 2.1 o versiones posteriores
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 el proyecto completado. Una buena forma de obtener ayuda consiste en publicar una pregunta en
StackOverflow.com para ASP.NET Core o EF Core.

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 la aplicación web de Razor Pages ContosoUniversity


Visual Studio
CLI de .NET Core
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.1 en la lista desplegable y, luego, Aplicación web.
Para ver las imágenes de los pasos anteriores, consulte Creación de una aplicación web de Razor. Ejecute la
aplicación.

Configurar el estilo del sitio


Con algunos cambios se configura el menú del sitio, el diseño y la página principal. Actualice
Pages/Shared/_Layout.cshtml con 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>

<partial name="_CookieConsentPartial" />

<div class="container body-content">


@RenderBody()
<hr />
<footer>
<p>&copy; 2018 : Contoso University</p>
</footer>
</div>

@*Remaining markup not shown for brevity.*@

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>

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. En el ejemplo anterior, la clave principal
alternativa que se reconoce de forma automática es StudentID .
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.

Aplicación de scaffolding al modelo de alumnos


En esta sección, se aplica scaffolding al modelo de alumnos. Es decir, la herramienta de scaffolding genera páginas
para las operaciones de creación, lectura, actualización y eliminación (CRUD ) del modelo de alumnos.
Compile el proyecto.
Cree la carpeta Pages/Students.
Visual Studio
CLI de .NET Core
En el Explorador de soluciones, haga clic con el botón derecho en la carpeta Pages/Students > Agregar >
Nuevo elemento con scanffold.
En el cuadro de diálogo Agregar scaffold, seleccione Razor Pages using Entity Framework (CRUD )
[Páginas de Razor Pages que usan Entity Framework (CRUD )] > AGREGAR.

Complete el cuadro de diálogo para agregar páginas de Razor Pages que usan Entity Framework (CRUD ):
En la lista desplegable Clase de modelo, seleccione Student (ContosoUniversity.Models).
En la fila Clase de contexto de datos, haga clic en el signo + (más) y cambie el nombre generado por
ContosoUniversity.Models.SchoolContext.
En la lista desplegable Clase de contexto de datos, seleccione ContosoUniversity.Models.SchoolContext
Seleccione Agregar.
Si tiene algún problema con el paso anterior, consulte Aplicar scaffolding al modelo de película.
El proceso de scaffolding ha creado y cambiado los archivos siguientes:
Archivos creados
Pages/Students Create, Delete, Details, Edit, Index.
Data/SchoolContext.cs
Actualizaciones de archivos
Startup.cs: en la sección siguiente se detallan los cambios realizados en este archivo.
appsettings.json: se agrega la cadena de conexión que se usa para conectarse a una base de datos local.

Examinar el contexto registrado con la inserción de dependencias


ASP.NET Core integra 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.
La herramienta de scaffolding creó de forma automática un contexto de base de datos y lo registró con el
contenedor de inserción de dependencias.
Examine el método ConfigureServices de Startup.cs. El proveedor de scaffolding ha agregado la línea resaltada:
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);

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

El nombre de la cadena de conexión se pasa al contexto mediante una llamada a un método en un objeto
DbContextOptions. Para el desarrollo local, el sistema de configuración de ASP.NET Core lee la cadena de conexión
desde el archivo appsettings.json.

Actualización de main
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.
Llame a EnsureCreated.
Elimine el contexto cuando finalice el método EnsureCreated .

En el código siguiente se muestra el archivo Program.cs actualizado.


using ContosoUniversity.Models; // SchoolContext
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection; // CreateScope
using Microsoft.Extensions.Logging;
using System;

namespace ContosoUniversity
{
public class Program
{
public static void Main(string[] args)
{
var host = CreateWebHostBuilder(args).Build();

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


{
var services = scope.ServiceProvider;

try
{
var context = services.GetRequiredService<SchoolContext>();
context.Database.EnsureCreated();
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred creating the DB.");
}
}

host.Run();
}

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>


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

EnsureCreated garantiza la existencia de la base de datos para el contexto. Si existe, no se realiza ninguna acción. Si
no existe, se crean la base de datos y todo su esquema. En EnsureCreated no se usan migraciones para crear la
base de datos. Una base de datos que se cree con EnsureCreated no se podrá actualizar más adelante mediante las
migraciones.
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 .

EnsureCreated es útil al principio del desarrollo, cuando el esquema evoluciona rápidamente. Más adelante, en el
tutorial se elimina la base de datos y se usan las migraciones.
Prueba de la aplicación
Ejecute la aplicación y acepte la directiva de cookies. Esta aplicación no conserva información de carácter personal.
Puede obtener más información sobre la directiva de cookies en Compatibilidad con el Reglamento general de
protección de datos (RGPD ) de la UE.
Haga clic en el vínculo Students y, después, en Crear nuevo.
Pruebe los vínculos Edit, Details y Delete.

Examinar 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 .
Actualice SchoolContext.cs con el código siguiente:

using Microsoft.EntityFrameworkCore;

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

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


public DbSet<Enrollment> Enrollment { get; set; }
public DbSet<Course> Course { get; set; }
}
}

El código resaltado crea una propiedad DbSet<TEntity > 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 .
SQL Server Express LocalDB
La cadena de conexión especifica 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 Initialize para rellenarlo 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.Student.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.Student.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.Course.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.Enrollment.Add(e);
}
context.SaveChanges();
}
}
}
El código comprueba si hay estudiantes en la base de datos. Si no hay alumnos 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 llame a Initialize :

public class Program


{
public static void Main(string[] args)
{
var host = CreateWebHostBuilder(args).Build();

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


{
var services = scope.ServiceProvider;

try
{
var context = services.GetRequiredService<SchoolContext>();
// using ContosoUniversity.Data;
DbInitializer.Initialize(context);
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred creating the DB.");
}
}

host.Run();
}

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>


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

Elimine los registros de los alumnos y reinicie la aplicación. Si la base de datos no se ha inicializado, establezca un
punto de interrupción en Initialize para diagnosticar el problema.

Ver la base de datos


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.

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.Student.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 Programación asincrónica y
Programación asincrónica con async y await.
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)
06/08/2018 • 21 minutes to read • Edit Online

La versión de ASP.NET Core 2.0 de este tutorial se puede encontrar en este archivo PDF.
La versión de ASP.NET Core 2.1 de este tutorial presenta muchas mejoras con respecto a la versión 2.0.
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.
Para minimizar la complejidad y mantener estos tutoriales centrados en EF Core, en los modelos de página se usa
código de EF Core. Algunos desarrolladores usan un patrón de capa o patrón de repositorio de servicio para crear
una capa de abstracción entre la interfaz de usuario (Razor Pages) y la capa de acceso a datos.
En este tutorial, se examinan las Razor Pages 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

SingleOrDefaultAsync frente a FirstOrDefaultAsync


En el código generado se usa FirstOrDefaultAsync, que normalmente es preferible a SingleOrDefaultAsync.
FirstOrDefaultAsync es más eficaz que SingleOrDefaultAsync 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.

FindAsync
En gran parte del código con scaffolding, se puede usar FindAsync en lugar de FirstOrDefaultAsync .
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 se produce en aplicaciones web
normales.
Usa implícitamente FirstAsync en lugar de SingleAsync.
Sin embargo, si quiere aplicar Include a otras entidades, FindAsync ya no resulta apropiado. Esto significa que
puede que necesite descartar FindAsync 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>

Ejecute la aplicación y 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.Student


.Include(s => s.Enrollments)
.ThenInclude(e => e.Course)
.AsNoTracking()
.FirstOrDefaultAsync(m => m.ID == id);

if (Student == null)
{
return NotFound();
}
return Page();
}
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 . 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:
@page "{id:int}"
@model ContosoUniversity.Pages.Students.DetailsModel

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

<h2>Details</h2>

<div>
<h4>Student</h4>
<hr />
<dl class="dl-horizontal">
<dt>
@Html.DisplayNameFor(model => model.Student.LastName)
</dt>
<dd>
@Html.DisplayFor(model => model.Student.LastName)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Student.FirstMidName)
</dt>
<dd>
@Html.DisplayFor(model => model.Student.FirstMidName)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Student.EnrollmentDate)
</dt>
<dd>
@Html.DisplayFor(model => model.Student.EnrollmentDate)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Student.Enrollments)
</dt>
<dd>
<table class="table">
<tr>
<th>Course Title</th>
<th>Grade</th>
</tr>
@foreach (var item in Model.Student.Enrollments)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Course.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.Grade)
</td>
</tr>
}
</table>
</dd>
</dl>
</div>
<div>
<a asp-page="./Edit" asp-route-id="@Model.Student.ID">Edit</a> |
<a asp-page="./Index">Back to List</a>
</div>

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.Student.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 SchoolContext _context;

public EditModel(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.Student.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.Student.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 SaveChangesAsync. Por ejemplo, cuando se pasa una entidad nueva al
método AddAsync, el estado de esa entidad se establece en Added. Cuando se llama a SaveChangesAsync , 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 SchoolContext _context;

public DeleteModel(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.Student


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


.AsNoTracking()
.FirstOrDefaultAsync(m => m.ID == id);

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

try
{
_context.Student.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, 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/Index 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/Index 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)
24/09/2018 • 25 minutes to read • Edit Online

La versión de ASP.NET Core 2.0 de este tutorial se puede encontrar en este archivo PDF.
La versión de ASP.NET Core 2.1 de este tutorial presenta muchas mejoras con respecto a la versión 2.0.
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.

Agregar ordenación a la página de índice


Agregue cadenas al PageModel de Students/Index.cshtml.cs para que contenga los parámetros de ordenación:
public class IndexModel : PageModel
{
private readonly SchoolContext _context;

public IndexModel(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.Student


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. El asistente 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.Student


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 ?: condicional 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.Student


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 se podría detallar con un gran número de columnas ordenables.


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


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 el asistente de etiquetas <form> para agregar el cuadro de texto de búsqueda y el botón. De
forma predeterminada, el asistente 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.Student


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. 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">
<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 asistentes 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 un modelo de vista para los datos usados por la página About.
Actualice la página About para usar el modelo de vista.
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.Models.SchoolViewModels;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ContosoUniversity.Models;
using ContosoUniversity.Data;

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

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)
16/07/2018 • 10 minutes to read • Edit Online

La versión de ASP.NET Core 2.0 de este tutorial se puede encontrar en este archivo PDF.
La versión de ASP.NET Core 2.1 de este tutorial presenta muchas mejoras con respecto a la versión 2.0.
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.
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.

Eliminación de la base de datos


Use el Explorador de objetos de SQL Server (SSOX) o el comando database drop :
Visual Studio
CLI de .NET Core
En la Consola del Administrador de paquetes (PMC ), ejecute el comando siguiente:

Drop-Database

Ejecute Get-Help about_EntityFrameworkCore desde PMC para obtener información de ayuda.

Creación de una migración inicial y actualización de la base de datos


Compile el proyecto y cree la primera migración.
Visual Studio
CLI de .NET Core

Add-Migration InitialCreate
Update-Database

Examinar los métodos Up y Down


El comando migrations add de EF Core ha generado código para 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>(nullable: false),
Title = table.Column<string>(nullable: true),
Credits = table.Column<int>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Course", x => x.CourseID);
});

migrationBuilder.CreateTable(
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 base de datos se eliminó, de modo que ya no existe y ahora se crea mediante las migraciones.
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.
Para eliminar una migración, use el comando siguiente:
Visual Studio
CLI de .NET Core
Remove-Migration
El comando remove migrations elimina la migración y garantiza que la instantánea se restablece correctamente.
Eliminación de EnsureCreated y prueba de la aplicación
Para el desarrollo inicial se ha utilizado 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();

Ejecute la aplicación y compruebe que la base de datos se haya inicializado.


Inspección de la base de datos
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 ninguna migración.

Solución de problemas
Descargue la aplicación completada.
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

Recursos adicionales
CLI de .NET Core.
Consola del administrador de paquetes (Visual Studio)

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)
02/08/2018 • 48 minutes to read • Edit Online

La versión de ASP.NET Core 2.0 de este tutorial se puede encontrar en este archivo PDF.
La versión de ASP.NET Core 2.1 de este tutorial presenta muchas mejoras con respecto a la versión 2.0.
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.

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:
Visual Studio
CLI de .NET Core

Add-Migration ColumnFirstName
Update-Database
El comando 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; }
}
}

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 corresponde a 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.Models
{
public class SchoolContext : DbContext
{
public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
{
}

public DbSet<Course> Courses { get; set; }


public DbSet<Enrollment> Enrollment { get; set; }
public DbSet<Student> Student { 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.Student.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.Student.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.Enrollment.Where(
s =>
s.Student.ID == e.StudentID &&
s.Course.CourseID == e.CourseID).SingleOrDefault();
if (enrollmentInDataBase == null)
{
context.Enrollment.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

Agregar una migración


Compile el proyecto.
Visual Studio
CLI de .NET Core

Add-Migration 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.
Eliminación y actualización de 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, quite y actualice la base de datos:
Visual Studio
CLI de .NET Core
En la Consola del Administrador de paquetes (PMC ), ejecute el comando siguiente:

Drop-Database
Update-Database

Ejecute Get-Help about_EntityFrameworkCore desde PMC para obtener información de ayuda.


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:


Si anteriormente se abrió SSOX, haga clic en el botón Actualizar.
Expanda el nodo Tablas. Se muestran las tablas creadas.

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)
02/08/2018 • 25 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


Visual Studio
CLI de .NET Core
Siga las instrucciones que encontrará en Aplicación de scaffolding al modelo de alumnos y use Course para la
clase de modelo.
El comando anterior aplica scaffolding al modelo Course . Abra el proyecto en Visual Studio.
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


Visual Studio
CLI de .NET Core
Siga las instrucciones que encontrará en Aplicación de scaffolding al modelo de alumnos y use Instructor para
la clase de modelo.
El comando anterior aplica scaffolding al modelo Instructor . 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 el archivo Pages/Instructors/Index.cshtml.cs:

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/Instructors/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>
<a asp-page="./Index" asp-route-courseID="@item.CourseID">Select</a>
</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)
16/07/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
Páginas de Razor con EF Core en ASP.NET Core:
Simultaneidad (8 de 8)
02/08/2018 • 27 minutes to read • Edit Online

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:
rowversion no 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


Visual Studio
CLI de .NET Core
Siga las instrucciones que encontrará en Aplicación de scaffolding al modelo de alumnos y use Department para
la clase de modelo.
El comando anterior aplica scaffolding al modelo Department . Abra el proyecto en Visual Studio.
Compile el proyecto.
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
07/09/2018 • 42 minutes to read • Edit Online

Este tutorial no se ha actualizado para ASP.NET Core 2.1. Para acceder a la versión para ASP.NET Core 2.0 de
este tutorial, seleccione ASP.NET Core 2.0 encima de la tabla de contenido o en la parte superior de la página:
La versión de Razor Pages de ASP.NET Core 2.1 de este tutorial tiene muchas mejoras con respecto a la
versión 2.0.
En el tutorial para la versión 2.0 se explica el funcionamiento de ASP.NET Core MVC y Entity Framework Core
con los controladores y las vistas. Razor Pages 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. 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. Por ejemplo, el código de scaffolding se ha simplificado considerablemente.
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 Razor Pages, le agradeceremos que nos explique el motivo en
esta discusión de GitHub.
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
Instale uno de los siguientes:
Herramientas de CLI: Windows, Linux o macOS: .NET Core SDK 2.0 o posterior
Herramientas de IDE/editor
Windows: Visual Studio para Windows
Carga de trabajo de ASP.NET y desarrollo web
Carga de trabajo Desarrollo multiplataforma de .NET Core
Linux: Visual Studio Code
macOS: Visual Studio para 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" />
<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"
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 Core se encarga 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)
31/08/2018 • 39 minutes to read • Edit Online

Este tutorial no se ha actualizado para ASP.NET Core 2.1. Para acceder a la versión para ASP.NET Core 2.0 de
este tutorial, seleccione ASP.NET Core 2.0 encima de la tabla de contenido o en la parte superior de la página:
La versión de Razor Pages de ASP.NET Core 2.1 de este tutorial tiene muchas mejoras con respecto a la versión
2.0.
En el tutorial para la versión 2.0 se explica el funcionamiento de ASP.NET Core MVC y Entity Framework Core
con los controladores y las vistas. Razor Pages 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. 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. Por ejemplo, el código de scaffolding se ha simplificado considerablemente.
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 Razor Pages, le agradeceremos que nos explique el motivo en esta
discusión de GitHub.
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 patrón 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 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 . 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);
}

En este código se agrega la entidad Student creada por el enlazador de modelos de ASP.NET Core MVC al
conjunto de entidades Students y después se guardan los cambios en la base de datos. (El enlazador de modelos
se refiere a la funcionalidad de ASP.NET Core 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 Core. 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)
16/07/2018 • 28 minutes to read • Edit Online

Este tutorial no se ha actualizado para ASP.NET Core 2.1. Para acceder a la versión para ASP.NET Core 2.0 de
este tutorial, seleccione ASP.NET Core 2.0 encima de la tabla de contenido o en la parte superior de la página:
La versión de Razor Pages de ASP.NET Core 2.1 de este tutorial tiene muchas mejoras con respecto a la versión
2.0.
En el tutorial para la versión 2.0 se explica el funcionamiento de ASP.NET Core MVC y Entity Framework Core
con los controladores y las vistas. Razor Pages 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. 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. Por ejemplo, el código de scaffolding se ha simplificado considerablemente.
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 Razor Pages, le agradeceremos que nos explique el motivo en esta
discusión de GitHub.
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)
24/09/2018 • 16 minutes to read • Edit Online

Este tutorial no se ha actualizado para ASP.NET Core 2.1. Para acceder a la versión para ASP.NET Core 2.0 de
este tutorial, seleccione ASP.NET Core 2.0 encima de la tabla de contenido o en la parte superior de la página:
La versión de Razor Pages de ASP.NET Core 2.1 de este tutorial tiene muchas mejoras con respecto a la
versión 2.0.
En el tutorial para la versión 2.0 se explica el funcionamiento de ASP.NET Core MVC y Entity Framework Core
con los controladores y las vistas. Razor Pages 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. 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. Por ejemplo, el código de scaffolding se ha simplificado considerablemente.
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 Razor Pages, le agradeceremos que nos explique el motivo en
esta discusión de GitHub.
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)
24/09/2018 • 55 minutes to read • Edit Online

Este tutorial no se ha actualizado para ASP.NET Core 2.1. Para acceder a la versión para ASP.NET Core 2.0 de
este tutorial, seleccione ASP.NET Core 2.0 encima de la tabla de contenido o en la parte superior de la página:
La versión de Razor Pages de ASP.NET Core 2.1 de este tutorial tiene muchas mejoras con respecto a la versión
2.0.
En el tutorial para la versión 2.0 se explica el funcionamiento de ASP.NET Core MVC y Entity Framework Core
con los controladores y las vistas. Razor Pages 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. 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. Por ejemplo, el código de scaffolding se ha simplificado considerablemente.
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 Razor Pages, le agradeceremos que nos explique el motivo en esta
discusión de GitHub.
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 del asistente 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 Core 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)
16/07/2018 • 28 minutes to read • Edit Online

Este tutorial no se ha actualizado para ASP.NET Core 2.1. Para acceder a la versión para ASP.NET Core 2.0 de
este tutorial, seleccione ASP.NET Core 2.0 encima de la tabla de contenido o en la parte superior de la página:
La versión de Razor Pages de ASP.NET Core 2.1 de este tutorial tiene muchas mejoras con respecto a la versión
2.0.
En el tutorial para la versión 2.0 se explica el funcionamiento de ASP.NET Core MVC y Entity Framework Core
con los controladores y las vistas. Razor Pages 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. 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. Por ejemplo, el código de scaffolding se ha simplificado considerablemente.
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 Razor Pages, le agradeceremos que nos explique el motivo en esta
discusión de GitHub.
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)
16/07/2018 • 33 minutes to read • Edit Online

Este tutorial no se ha actualizado para ASP.NET Core 2.1. Para acceder a la versión para ASP.NET Core 2.0 de
este tutorial, seleccione ASP.NET Core 2.0 encima de la tabla de contenido o en la parte superior de la página:
La versión de Razor Pages de ASP.NET Core 2.1 de este tutorial tiene muchas mejoras con respecto a la versión
2.0.
En el tutorial para la versión 2.0 se explica el funcionamiento de ASP.NET Core MVC y Entity Framework Core
con los controladores y las vistas. Razor Pages 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. 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. Por ejemplo, el código de scaffolding se ha simplificado considerablemente.
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 Razor Pages, le agradeceremos que nos explique el motivo en esta
discusión de GitHub.
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)
16/07/2018 • 35 minutes to read • Edit Online

Este tutorial no se ha actualizado para ASP.NET Core 2.1. Para acceder a la versión para ASP.NET Core 2.0 de
este tutorial, seleccione ASP.NET Core 2.0 encima de la tabla de contenido o en la parte superior de la página:
La versión de Razor Pages de ASP.NET Core 2.1 de este tutorial tiene muchas mejoras con respecto a la versión
2.0.
En el tutorial para la versión 2.0 se explica el funcionamiento de ASP.NET Core MVC y Entity Framework Core
con los controladores y las vistas. Razor Pages 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. 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. Por ejemplo, el código de scaffolding se ha simplificado considerablemente.
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 Razor Pages, le agradeceremos que nos explique el motivo en esta
discusión de GitHub.
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)
16/07/2018 • 16 minutes to read • Edit Online

Este tutorial no se ha actualizado para ASP.NET Core 2.1. Para acceder a la versión para ASP.NET Core 2.0 de
este tutorial, seleccione ASP.NET Core 2.0 encima de la tabla de contenido o en la parte superior de la página:
La versión de Razor Pages de ASP.NET Core 2.1 de este tutorial tiene muchas mejoras con respecto a la versión
2.0.
En el tutorial para la versión 2.0 se explica el funcionamiento de ASP.NET Core MVC y Entity Framework Core
con los controladores y las vistas. Razor Pages 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. 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. Por ejemplo, el código de scaffolding se ha simplificado considerablemente.
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 Razor Pages, le agradeceremos que nos explique el motivo en esta
discusión de GitHub.
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)
24/09/2018 • 26 minutes to read • Edit Online

Este tutorial no se ha actualizado para ASP.NET Core 2.1. Para acceder a la versión para ASP.NET Core 2.0 de
este tutorial, seleccione ASP.NET Core 2.0 encima de la tabla de contenido o en la parte superior de la página:
La versión de Razor Pages de ASP.NET Core 2.1 de este tutorial tiene muchas mejoras con respecto a la versión
2.0.
En el tutorial para la versión 2.0 se explica el funcionamiento de ASP.NET Core MVC y Entity Framework Core
con los controladores y las vistas. Razor Pages 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. 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. Por ejemplo, el código de scaffolding se ha simplificado considerablemente.
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 Razor Pages, le agradeceremos que nos explique el motivo en esta
discusión de GitHub.
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
Core 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
24/09/2018 • 4 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 para 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

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
16/07/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)


{
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.1.4" />
</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


Agregue las líneas siguientes al archivo RazorPagesMovie.csproj, justo antes de la etiqueta </Project> de cierre:

<ItemGroup>
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.1.0-preview1-
final"/>
</ItemGroup>

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 elemento DotNetCliToolReference y el comando add package instalan 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
16/07/2018 • 14 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:

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

namespace RazorPagesMovie.Pages.Movies
{
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 async Task OnGetAsync()


{
Movie = await _context.Movie.ToListAsync();
}
}
}
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Models;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace RazorPagesMovie.Pages.Movies
{
public class IndexModel : PageModel
{
private readonly RazorPagesMovie.Models.RazorPagesMovieContext _context;

public IndexModel(RazorPagesMovie.Models.RazorPagesMovieContext context)


{
_context = context;
}

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

public async Task OnGetAsync()


{
Movie = await _context.Movie.ToListAsync();
}
}
}

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 el siguiente asistente de HTML:
@Html.DisplayNameFor(model => model.Movie[0].Title))

El asistente de 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 los
asistentes de 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/Shared/_Layout.cshtml. En el siguiente marcado se muestran las
primeras líneas del archivo Pages/Shared/_Layout.cshtml.
La propiedad "Title" se usa en el archivo Pages/Shared/_Layout.cshtml. En el siguiente marcado se muestran las
primeras líneas del archivo _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.

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.

La propiedad Layout se establece en el archivo Pages/_ViewStart.cshtml:

@{
Layout = "_Layout";
}

El marcado anterior establece el archivo de diseño en Pages/Shared/_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/Shared/_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 un asistente de etiquetas. En este caso, se trata de el asistente de etiquetas
Anchor. El atributo y valor del asistente 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");
}
}
}
// 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.RazorPagesMovieContext _context;

public CreateModel(RazorPagesMovie.Models.RazorPagesMovieContext 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 que
inicializar, de modo que se devuelve Page . Más adelante en el tutorial veremos el estado de inicialización del
método OnGet . 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 en una aplicación de Razor
Pages de ASP.NET Core
16/07/2018 • 3 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
16/07/2018 • 9 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; }
}
}
Haga clic con el botón derecho en una línea ondulada roja > Acciones rápidas y refactorizaciones en el
atributo [Column] y seleccione using System.ComponentModel.DataAnnotations.Schema; .
La anotación de datos [Column(TypeName = "decimal(18, 2)")] es necesaria para que Entity Framework Core
asigne correctamente Price a la moneda en la base de datos. Para más información, vea Tipos de datos.
Modelo completado:

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

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

[Column(TypeName = "decimal(18, 2)")]


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

Los asistentes 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:
public class EditModel : PageModel
{
private readonly RazorPagesMovie.Models.MovieContext _context;

public EditModel(RazorPagesMovie.Models.MovieContext context)


{
_context = context;
}

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

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


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

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

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

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");
}
}
public class EditModel : PageModel
{
private readonly RazorPagesMovie.Models.RazorPagesMovieContext _context;

public EditModel(RazorPagesMovie.Models.RazorPagesMovieContext context)


{
_context = context;
}

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

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


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

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

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

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

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
Agregar búsqueda a una aplicación de Razor Pages
de ASP.NET Core
16/07/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
24/09/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
Instale el software siguiente:
.NET Core SDK 2.0 o posterior
Visual Studio Code
C# para Visual Studio Code
SDK de .NET Core 2.1 o versiones posteriores
Visual Studio Code
C# para 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

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
07/09/2018 • 8 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.
Paquetes NuGet de Entity Framework Core para SQLite
Desde la línea de comandos, ejecute el siguiente comando de la CLI de .NET Core:

dotnet add package Microsoft.EntityFrameworkCore.SQLite

public void ConfigureServices(IServiceCollection services)


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

Agregue las instrucciones using siguientes en la parte superior de Startup.cs:

using RazorPagesMovie.Models;
using Microsoft.EntityFrameworkCore;

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.1.4" />
</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


Agregue las líneas siguientes al archivo RazorPagesMovie.csproj, justo antes de la etiqueta </Project> de cierre:

<ItemGroup>
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.1.0-preview1-
final"/>
</ItemGroup>

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 elemento DotNetCliToolReference y el comando add package instalan 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
16/07/2018 • 14 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:

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

namespace RazorPagesMovie.Pages.Movies
{
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 async Task OnGetAsync()


{
Movie = await _context.Movie.ToListAsync();
}
}
}
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Models;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace RazorPagesMovie.Pages.Movies
{
public class IndexModel : PageModel
{
private readonly RazorPagesMovie.Models.RazorPagesMovieContext _context;

public IndexModel(RazorPagesMovie.Models.RazorPagesMovieContext context)


{
_context = context;
}

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

public async Task OnGetAsync()


{
Movie = await _context.Movie.ToListAsync();
}
}
}

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 el siguiente asistente de HTML:
@Html.DisplayNameFor(model => model.Movie[0].Title))

El asistente de 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 los
asistentes de 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/Shared/_Layout.cshtml. En el siguiente marcado se muestran las
primeras líneas del archivo Pages/Shared/_Layout.cshtml.
La propiedad "Title" se usa en el archivo Pages/Shared/_Layout.cshtml. En el siguiente marcado se muestran las
primeras líneas del archivo _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.

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.

La propiedad Layout se establece en el archivo Pages/_ViewStart.cshtml:

@{
Layout = "_Layout";
}

El marcado anterior establece el archivo de diseño en Pages/Shared/_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/Shared/_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 un asistente de etiquetas. En este caso, se trata de el asistente de etiquetas
Anchor. El atributo y valor del asistente 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");
}
}
}
// 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.RazorPagesMovieContext _context;

public CreateModel(RazorPagesMovie.Models.RazorPagesMovieContext 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 que
inicializar, de modo que se devuelve Page . Más adelante en el tutorial veremos el estado de inicialización del
método OnGet . 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 en una aplicación de Razor
Pages de ASP.NET Core
16/07/2018 • 3 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
16/07/2018 • 9 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; }
}
}
Haga clic con el botón derecho en una línea ondulada roja > Acciones rápidas y refactorizaciones en el
atributo [Column] y seleccione using System.ComponentModel.DataAnnotations.Schema; .
La anotación de datos [Column(TypeName = "decimal(18, 2)")] es necesaria para que Entity Framework Core
asigne correctamente Price a la moneda en la base de datos. Para más información, vea Tipos de datos.
Modelo completado:

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

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

[Column(TypeName = "decimal(18, 2)")]


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

Los asistentes 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:
public class EditModel : PageModel
{
private readonly RazorPagesMovie.Models.MovieContext _context;

public EditModel(RazorPagesMovie.Models.MovieContext context)


{
_context = context;
}

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

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


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

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

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

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");
}
}
public class EditModel : PageModel
{
private readonly RazorPagesMovie.Models.RazorPagesMovieContext _context;

public EditModel(RazorPagesMovie.Models.RazorPagesMovieContext context)


{
_context = context;
}

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

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


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

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

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

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

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
Agregar búsqueda a una aplicación de Razor Pages
de ASP.NET Core
16/07/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. Razor Pages es
una característica del marco de ASP.NET Core MVC que facilita y rentabiliza la compilación y las pruebas en la
interfaz de usuario web. Puede usar Razor Pages con controladores y vistas en el mismo proyecto.
Se recomienda probar el tutorial de Razor Pages antes que la versión de MVC, controladores o vistas. 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. Razor Pages es
una característica del marco de ASP.NET Core MVC que facilita y rentabiliza la compilación y las pruebas en la
interfaz de usuario web. Puede usar Razor Pages con controladores y vistas en el mismo proyecto.
Se recomienda probar el tutorial de Razor Pages antes que la versión de MVC, controladores o vistas. 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 para 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 • 12 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
Agregar una vista a una aplicación de ASP.NET Core
MVC
16/07/2018 • 16 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í:

@inject Microsoft.ApplicationInsights.AspNetCore.JavaScriptSnippet JavaScriptSnippet


<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - Movie App</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" />
<link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" />
</environment>
@Html.Raw(JavaScriptSnippet.FullScript)
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<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="Movies" asp-action="Index" class="navbar-brand">Movie App</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>
</div>
</div>
</nav>
<div class="container body-content">
@RenderBody()
<hr />
<footer>
<p>&copy; 2017 - MvcMovie</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>

@inject Microsoft.ApplicationInsights.AspNetCore.JavaScriptSnippet JavaScriptSnippet


<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - Movie App</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-
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>
@Html.Raw(JavaScriptSnippet.FullScript)
</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="Movies" asp-action="Index" class="navbar-brand">Movie App</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>
</div>
</div>
</nav>
<div class="container body-content">
@RenderBody()
<hr />
<footer>
<p>&copy; 2017 - MvcMovie</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>

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
02/07/2018 • 9 minutes to read • Edit Online

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.1.4" />
<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 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. Para más información, vea https://github.com/aspnet/Docs/issues/4076 y Recursos
adicionales. Por ahora, escriba solamente 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 una aplicación de ASP.NET
Core MVC
04/07/2018 • 3 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:

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

namespace MvcMovie
{
public class Program
{
public static void Main(string[] args)
{
var host = CreateWebHostBuilder(args).Build();

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


{
var services = scope.ServiceProvider;

try
{
var context = services.GetRequiredService<MvcMovieContext>();
context.Database.Migrate();
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 IWebHostBuilder CreateWebHostBuilder(string[] args) =>


WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
}
}
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using MvcMovie.Models;
using System;

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

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


{
var services = scope.ServiceProvider;

try
{
// Requires using MvcMovie.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 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 • 16 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.
La anotación de datos [Column(TypeName = "decimal(18, 2)")] es necesaria para que Entity Framework Core
asigne correctamente Price a la moneda en la base de datos. Para más información, vea Tipos de datos.
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 el asistente 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>

Los asistentes 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).
Los asistentes 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.FindAsync(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);
}

// 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] .
[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(nameof(Index));
}
return View(movie);
}
// 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 el asistente de etiquetas de formulario.

<form asp-action="Edit">

El asistente 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.FindAsync(id);


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

// 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 del asistente de etiquetas para simplificar el marcado HTML. El
asistente de etiquetas muestra el nombre del campo: "Title" (Título), "ReleaseDate" (Fecha de lanzamiento),
"Genre" (Género) o "Price" (Precio). El asistente de etiquetas de entrada representa un elemento HTML <input> .
El asistente 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 el asistente 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 .
[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(nameof(Index));
}
return View(movie);
}
// 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 el asistente 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. El asistente 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 los asistentes de etiquetas
Creación de asistentes de etiquetas
Prevención de ataques de falsificación de solicitudes
Protección del controlador frente al exceso de publicación
ViewModels
Asistente de etiquetas de formulario
Asistente de etiquetas de entrada
Asistente de etiquetas de elementos de etiqueta
Asistente de etiquetas de selección
Asistente 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
Agregar búsqueda a una aplicación de ASP.NET Core
MVC
04/07/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
Agregar un campo nuevo a una aplicación de
ASP.NET Core MVC
02/07/2018 • 6 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 :

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

[Column(TypeName = "decimal(18, 2)")]


public decimal Price { get; set; }
public string Rating { get; set; }
}

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

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
Agregar validación a una aplicación ASP.NET Core
MVC
04/07/2018 • 19 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 Core MVC le anima a que
especifique la funcionalidad o el comportamiento una sola vez y a que luego los 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 .
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)]
[Column(TypeName = "decimal(18, 2)")]
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; }
}

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 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 solo pueden usar
letras (no se permiten mayúsculas iniciales, 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] .
El que ASP.NET Core aplique automáticamente las reglas de validación ayuda a que su 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>

El asistente de etiquetas de entrada usa los atributos DataAnnotations y genera los atributos HTML necesarios
para la validación de jQuery en el lado cliente. El asistente 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 con el guion) 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:

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


[Column(TypeName = "decimal(18, 2)")]
public decimal Price { get; set; }

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

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

[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-]*$"), Required, StringLength(5)]


public string Rating { get; set; }
}

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 los asistentes de etiquetas
Creación de asistentes 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
24/09/2018 • 6 minutes to read • Edit Online

Por Rick Anderson


Abra el controlador Movie y examine el método Details :

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

var movie = await _context.Movie


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

return View(movie);
}

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

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

var movie = await _context.Movie


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

return View(movie);
}

// POST: Movies/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
var movie = await _context.Movie.FindAsync(id);
_context.Movie.Remove(movie);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
// GET: Movies/Delete/5
public async Task<IActionResult> Delete(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);
}

// POST: Movies/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
var movie = await _context.Movie.SingleOrDefaultAsync(m => m.ID == id);
_context.Movie.Remove(movie);
await _context.SaveChangesAsync();
return RedirectToAction("Index");
}

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. Razor Pages es
una característica del marco de ASP.NET Core MVC que facilita y rentabiliza la compilación y las pruebas en la
interfaz de usuario web. Puede usar Razor Pages con controladores y vistas en el mismo proyecto.
Se recomienda probar el tutorial de Razor Pages antes que la versión de MVC, controladores o vistas. 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
31/08/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. Razor Pages es
una característica del marco de ASP.NET Core MVC que facilita y rentabiliza la compilación y las pruebas en la
interfaz de usuario web. Puede usar Razor Pages con controladores y vistas en el mismo proyecto.
Se recomienda probar el tutorial de Razor Pages antes que la versión de MVC, controladores o vistas. 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
Instale el software siguiente:
.NET Core SDK 2.0 o posterior
Visual Studio Code
C# para Visual Studio Code
SDK de .NET Core 2.1 o versiones posteriores
Visual Studio Code
C# para 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 • 12 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
Agregar una vista a una aplicación de ASP.NET Core
MVC
02/07/2018 • 16 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í:

@inject Microsoft.ApplicationInsights.AspNetCore.JavaScriptSnippet JavaScriptSnippet


<!DOCTYPE html>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - Movie App</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" />
<link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" />
</environment>
@Html.Raw(JavaScriptSnippet.FullScript)
</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="Movies" asp-action="Index" class="navbar-brand">Movie App</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>
</div>
</div>
</nav>
<div class="container body-content">
@RenderBody()
<hr />
<footer>
<p>&copy; 2017 - MvcMovie</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>
</environment>

@RenderSection("Scripts", required: false)


</body>
</html>

@inject Microsoft.ApplicationInsights.AspNetCore.JavaScriptSnippet JavaScriptSnippet


<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - Movie App</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" />
<link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" />
</environment>
@Html.Raw(JavaScriptSnippet.FullScript)
</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="Movies" asp-action="Index" class="navbar-brand">Movie App</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>
</div>
</div>
</nav>
<div class="container body-content">
@RenderBody()
<hr />
<footer>
<p>&copy; 2017 - MvcMovie</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">
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>

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
02/07/2018 • 8 minutes to read • Edit Online

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.1.4" />
<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 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. Para más información, vea https://github.com/aspnet/Docs/issues/4076 y Recursos
adicionales. Por ahora, escriba solamente 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 una aplicación de ASP.NET
Core MVC
04/07/2018 • 3 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:

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

namespace MvcMovie
{
public class Program
{
public static void Main(string[] args)
{
var host = CreateWebHostBuilder(args).Build();

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


{
var services = scope.ServiceProvider;

try
{
var context = services.GetRequiredService<MvcMovieContext>();
context.Database.Migrate();
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 IWebHostBuilder CreateWebHostBuilder(string[] args) =>


WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
}
}
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using MvcMovie.Models;
using System;

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

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


{
var services = scope.ServiceProvider;

try
{
// Requires using MvcMovie.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 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 • 16 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.
La anotación de datos [Column(TypeName = "decimal(18, 2)")] es necesaria para que Entity Framework Core
asigne correctamente Price a la moneda en la base de datos. Para más información, vea Tipos de datos.
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 el asistente 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>

Los asistentes 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).
Los asistentes 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.FindAsync(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);
}

// 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] .
[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(nameof(Index));
}
return View(movie);
}
// 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 el asistente de etiquetas de formulario.

<form asp-action="Edit">

El asistente 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.FindAsync(id);


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

// 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 del asistente de etiquetas para simplificar el marcado HTML. El
asistente de etiquetas muestra el nombre del campo: "Title" (Título), "ReleaseDate" (Fecha de lanzamiento),
"Genre" (Género) o "Price" (Precio). El asistente de etiquetas de entrada representa un elemento HTML <input> .
El asistente 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 el asistente 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 .
[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(nameof(Index));
}
return View(movie);
}
// 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 el asistente 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. El asistente 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 los asistentes de etiquetas
Creación de asistentes de etiquetas
Prevención de ataques de falsificación de solicitudes
Protección del controlador frente al exceso de publicación
ViewModels
Asistente de etiquetas de formulario
Asistente de etiquetas de entrada
Asistente de etiquetas de elementos de etiqueta
Asistente de etiquetas de selección
Asistente 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
Agregar búsqueda a una aplicación de ASP.NET Core
MVC
04/07/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
Agregar un campo nuevo a una aplicación de
ASP.NET Core MVC
02/07/2018 • 6 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 :

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

[Column(TypeName = "decimal(18, 2)")]


public decimal Price { get; set; }
public string Rating { get; set; }
}

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

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
Agregar validación a una aplicación ASP.NET Core
MVC
04/07/2018 • 19 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 Core MVC le anima a que
especifique la funcionalidad o el comportamiento una sola vez y a que luego los 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 .
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)]
[Column(TypeName = "decimal(18, 2)")]
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; }
}

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 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 solo pueden usar
letras (no se permiten mayúsculas iniciales, 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] .
El que ASP.NET Core aplique automáticamente las reglas de validación ayuda a que su 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>

El asistente de etiquetas de entrada usa los atributos DataAnnotations y genera los atributos HTML necesarios
para la validación de jQuery en el lado cliente. El asistente 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 con el guion) 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:

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


[Column(TypeName = "decimal(18, 2)")]
public decimal Price { get; set; }

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

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

[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-]*$"), Required, StringLength(5)]


public string Rating { get; set; }
}

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 los asistentes de etiquetas
Creación de asistentes 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
24/09/2018 • 6 minutes to read • Edit Online

Por Rick Anderson


Abra el controlador Movie y examine el método Details :

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

var movie = await _context.Movie


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

return View(movie);
}

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

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

var movie = await _context.Movie


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

return View(movie);
}

// POST: Movies/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
var movie = await _context.Movie.FindAsync(id);
_context.Movie.Remove(movie);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
// GET: Movies/Delete/5
public async Task<IActionResult> Delete(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);
}

// POST: Movies/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
var movie = await _context.Movie.SingleOrDefaultAsync(m => m.ID == id);
_context.Movie.Remove(movie);
await _context.SaveChangesAsync();
return RedirectToAction("Index");
}

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
24/09/2018 • 28 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 CLR 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 para 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:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using TodoApi.Models;

namespace TodoApi
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<TodoContext>(opt =>
opt.UseInMemoryDatabase("TodoList"));
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}

public void Configure(IApplicationBuilder app)


{
app.UseMvc();
}
}
}

using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using TodoApi.Models;

namespace TodoApi
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<TodoContext>(opt =>
opt.UseInMemoryDatabase("TodoList"));
services.AddMvc();
}

public void Configure(IApplicationBuilder app)


{
app.UseMvc();
}
}
}

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:
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Linq;
using TodoApi.Models;

namespace TodoApi.Controllers
{
[Route("api/[controller]")]
public class TodoController : ControllerBase
{
private readonly TodoContext _context;

public TodoController(TodoContext context)


{
_context = context;

if (_context.TodoItems.Count() == 0)
{
_context.TodoItems.Add(new TodoItem { Name = "Item1" });
_context.SaveChanges();
}
}
}
}

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.

using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Linq;
using TodoApi.Models;

namespace TodoApi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class TodoController : ControllerBase
{
private readonly TodoContext _context;

public TodoController(TodoContext context)


{
_context = context;

if (_context.TodoItems.Count() == 0)
{
// Create a new TodoItem if collection is empty,
// which means you can't delete all TodoItems.
_context.TodoItems.Add(new TodoItem { Name = "Item1" });
_context.SaveChanges();
}
}
}
}

El código anterior:
Define una clase de controlador de API sin métodos.
Crea una tarea pendiente cuando TodoItems está vacío. No podrá eliminar todas las tareas pendientes
porque el constructor crea una si TodoItems está vacío.
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 :

[HttpGet]
public List<TodoItem> GetAll()
{
return _context.TodoItems.ToList();
}

[HttpGet("{id}", Name = "GetTodo")]


public IActionResult GetById(long id)
{
var item = _context.TodoItems.Find(id);
if (item == null)
{
return NotFound();
}
return Ok(item);
}

[HttpGet]
public ActionResult<List<TodoItem>> GetAll()
{
return _context.TodoItems.ToList();
}

[HttpGet("{id}", Name = "GetTodo")]


public ActionResult<TodoItem> GetById(long id)
{
var item = _context.TodoItems.Find(id);
if (item == null)
{
return NotFound();
}
return item;
}

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:

namespace TodoApi.Controllers
{
[Route("api/[controller]")]
public class TodoController : ControllerBase
{
private readonly TodoContext _context;

namespace TodoApi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class TodoController : ControllerBase
{
private readonly TodoContext _context;

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.

[HttpGet("{id}", Name = "GetTodo")]


public IActionResult GetById(long id)
{
var item = _context.TodoItems.Find(id);
if (item == null)
{
return NotFound();
}
return Ok(item);
}

[HttpGet("{id}", Name = "GetTodo")]


public ActionResult<TodoItem> GetById(long id)
{
var item = _context.TodoItems.Find(id);
if (item == null)
{
return NotFound();
}
return item;
}

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
[HttpPost]
public IActionResult Create([FromBody] TodoItem item)
{
if (item == null)
{
return BadRequest();
}

_context.TodoItems.Add(item);
_context.SaveChanges();

return CreatedAtRoute("GetTodo", new { id = item.Id }, item);


}

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.

[HttpPost]
public IActionResult Create(TodoItem item)
{
_context.TodoItems.Add(item);
_context.SaveChanges();

return CreatedAtRoute("GetTodo", new { id = item.Id }, item);


}

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
[HttpPut("{id}")]
public IActionResult Update(long id, [FromBody] TodoItem item)
{
if (item == null || item.Id != id)
{
return BadRequest();
}

var todo = _context.TodoItems.Find(id);


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

todo.IsComplete = item.IsComplete;
todo.Name = item.Name;

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

[HttpPut("{id}")]
public IActionResult Update(long id, TodoItem item)
{
var todo = _context.TodoItems.Find(id);
if (todo == null)
{
return NotFound();
}

todo.IsComplete = item.IsComplete;
todo.Name = item.Name;

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

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')[0].checked = 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
06/08/2018 • 28 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 CLR 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
Instale el software siguiente:
.NET Core SDK 2.0 o posterior
Visual Studio Code
C# para Visual Studio Code
SDK de .NET Core 2.1 o versiones posteriores
Visual Studio Code
C# para 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.1 o posterior, se agrega la referencia de paquete
Microsoft.AspNetCore.App al archivo TodoApi.csproj. Agregue el atributo Version , si aún no se ha especificado.

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.4" />
</ItemGroup>

Al crear un proyecto en ASP.NET Core 2.0, se agrega la referencia de paquete Microsoft.AspNetCore.All al


archivo TodoApi.csproj:

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.1.4" />
</ItemGroup>

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:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using TodoApi.Models;

namespace TodoApi
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<TodoContext>(opt =>
opt.UseInMemoryDatabase("TodoList"));
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}

public void Configure(IApplicationBuilder app)


{
app.UseMvc();
}
}
}

using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using TodoApi.Models;

namespace TodoApi
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<TodoContext>(opt =>
opt.UseInMemoryDatabase("TodoList"));
services.AddMvc();
}

public void Configure(IApplicationBuilder app)


{
app.UseMvc();
}
}
}

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:
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Linq;
using TodoApi.Models;

namespace TodoApi.Controllers
{
[Route("api/[controller]")]
public class TodoController : ControllerBase
{
private readonly TodoContext _context;

public TodoController(TodoContext context)


{
_context = context;

if (_context.TodoItems.Count() == 0)
{
_context.TodoItems.Add(new TodoItem { Name = "Item1" });
_context.SaveChanges();
}
}
}
}

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.

using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Linq;
using TodoApi.Models;

namespace TodoApi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class TodoController : ControllerBase
{
private readonly TodoContext _context;

public TodoController(TodoContext context)


{
_context = context;

if (_context.TodoItems.Count() == 0)
{
// Create a new TodoItem if collection is empty,
// which means you can't delete all TodoItems.
_context.TodoItems.Add(new TodoItem { Name = "Item1" });
_context.SaveChanges();
}
}
}
}

El código anterior:
Define una clase de controlador de API sin métodos.
Crea una tarea pendiente cuando TodoItems está vacío. No podrá eliminar todas las tareas pendientes
porque el constructor crea una si TodoItems está vacío.
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 :

[HttpGet]
public List<TodoItem> GetAll()
{
return _context.TodoItems.ToList();
}

[HttpGet("{id}", Name = "GetTodo")]


public IActionResult GetById(long id)
{
var item = _context.TodoItems.Find(id);
if (item == null)
{
return NotFound();
}
return Ok(item);
}

[HttpGet]
public ActionResult<List<TodoItem>> GetAll()
{
return _context.TodoItems.ToList();
}

[HttpGet("{id}", Name = "GetTodo")]


public ActionResult<TodoItem> GetById(long id)
{
var item = _context.TodoItems.Find(id);
if (item == null)
{
return NotFound();
}
return item;
}

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:

namespace TodoApi.Controllers
{
[Route("api/[controller]")]
public class TodoController : ControllerBase
{
private readonly TodoContext _context;

namespace TodoApi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class TodoController : ControllerBase
{
private readonly TodoContext _context;

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.

[HttpGet("{id}", Name = "GetTodo")]


public IActionResult GetById(long id)
{
var item = _context.TodoItems.Find(id);
if (item == null)
{
return NotFound();
}
return Ok(item);
}

[HttpGet("{id}", Name = "GetTodo")]


public ActionResult<TodoItem> GetById(long id)
{
var item = _context.TodoItems.Find(id);
if (item == null)
{
return NotFound();
}
return item;
}

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');
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')[0].checked = 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 :

[HttpPost]
public IActionResult Create([FromBody] TodoItem item)
{
if (item == null)
{
return BadRequest();
}

_context.TodoItems.Add(item);
_context.SaveChanges();

return CreatedAtRoute("GetTodo", new { id = item.Id }, item);


}

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.

[HttpPost]
public IActionResult Create(TodoItem item)
{
_context.TodoItems.Add(item);
_context.SaveChanges();

return CreatedAtRoute("GetTodo", new { id = item.Id }, item);


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

[HttpGet("{id}", Name = "GetTodo")]


public IActionResult GetById(long id)
{
var item = _context.TodoItems.Find(id);
if (item == null)
{
return NotFound();
}
return Ok(item);
}

[HttpGet("{id}", Name = "GetTodo")]


public ActionResult<TodoItem> GetById(long id)
{
var item = _context.TodoItems.Find(id);
if (item == null)
{
return NotFound();
}
return item;
}

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 :

[HttpPut("{id}")]
public IActionResult Update(long id, [FromBody] TodoItem item)
{
if (item == null || item.Id != id)
{
return BadRequest();
}

var todo = _context.TodoItems.Find(id);


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

todo.IsComplete = item.IsComplete;
todo.Name = item.Name;

_context.TodoItems.Update(todo);
_context.SaveChanges();
return NoContent();
}
[HttpPut("{id}")]
public IActionResult Update(long id, TodoItem item)
{
var todo = _context.TodoItems.Find(id);
if (todo == null)
{
return NotFound();
}

todo.IsComplete = item.IsComplete;
todo.Name = item.Name;

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

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 un
monitor de archivos
16/07/2018 • 6 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 API web existente con dos puntos de conexión: uno que devuelve una suma y otro
que devuelve un producto. El método Product contiene un error, que se ha corregido en este tutorial.
Descargue la aplicación de ejemplo. Consta de dos proyectos: WebApp (una ASP.NET Core Web API) y
WebAppTests (pruebas unitarias para la API web).
En un shell de comandos, desplácese hasta la carpeta WebApp. 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. Ese problema se corregirá más adelante en el tutorial.

Agregar dotnet watch a un proyecto


La herramienta de monitor de archivos dotnet watch se incluye con la versión 2.1.300 del SDK de .NET Core. Si se
usa una versión anterior del SDK de .NET Core, será necesario realizar los siguientes pasos.
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
Ejecutar los comandos de la CLI de .NET Core con 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 .

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

Personalizar la lista de archivos que inspeccionar


dotnet-watch realiza un seguimiento de forma predeterminada de todos los archivos que coincidan con los
siguientes patrones globales:
**/*.cs
*.csproj
**/*.resx

Se pueden agregar más elementos a la lista de control inspección editando el archivo .csproj. Los elementos se
pueden especificar individualmente o usando patrones globales.

<ItemGroup>
<!-- extends watching group to include *.js files -->
<Watch Include="**\*.js" Exclude="node_modules\**\*;**\*.js.map;obj\**\*;bin\**\*" />
</ItemGroup>

Descartar archivos de la inspección


dotnet-watch se puede configurar para pasar por alto su configuración predeterminada. Para omitir archivos
concretos, agregue el atributo Watch="false" a la definición de un elemento en el archivo .csproj:

<ItemGroup>
<!-- exclude Generated.cs from dotnet-watch -->
<Compile Include="Generated.cs" Watch="false" />

<!-- exclude Strings.resx from dotnet-watch -->


<EmbeddedResource Include="Strings.resx" Watch="false" />

<!-- exclude changes in this referenced project -->


<ProjectReference Include="..\ClassLibrary1\ClassLibrary1.csproj" Watch="false" />
</ItemGroup>

Proyectos de inspección personalizados


dotnet-watch no queda restringido exclusivamente a proyectos de C#, sino que se pueden crear proyectos de
inspección personalizados para controlar distintos escenarios. Veamos el siguiente diseño de proyecto:
test/
UnitTests/UnitTests.csproj
IntegrationTests/IntegrationTests.csproj
Si el objetivo es inspeccionar los dos proyectos, cree un archivo de proyecto personalizado configurado para
supervisar ambos proyectos:

<Project>
<ItemGroup>
<TestProjects Include="**\*.csproj" />
<Watch Include="**\*.cs" />
</ItemGroup>

<Target Name="Test">
<MSBuild Targets="VSTest" Projects="@(TestProjects)" />
</Target>

<Import Project="$(MSBuildExtensionsPath)\Microsoft.Common.targets" />


</Project>

Para empezar a inspeccionar archivos en ambos proyectos, cambie a la carpeta test. Ejecute el siguiente comando:
dotnet watch msbuild /t:Test

VSTest se ejecuta cuando un archivo de cualquiera de los proyectos de prueba cambie.

dotnet-watch en GitHub
dotnet-watch forma parte del repositorio de DotNetTools de GitHub.
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
23/08/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 :

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 método Main invoca WebHost.CreateDefaultBuilder, que sigue el patrón del generador para crear un host de
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.

public class Program


{
public static void Main(string[] args)
{
var host = new WebHostBuilder()
.UseKestrel()
.UseStartup<Startup>()
.Build();

host.Run();
}
}

El método Main usa WebHostBuilder, que sigue el patrón del 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 usa el servidor web Kestrel. Si se invoca el método de extensión adecuado, se pueden
usar otros servidores web, como WebListener. UseStartup se explica en la sección siguiente.
WebHostBuilder proporciona muchos métodos opcionales, incluido UseIISIntegration para hospedar en IIS e IIS
Express, 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:
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>


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

public class Program


{
public static void Main(string[] args)
{
var host = new WebHostBuilder()
.UseKestrel()
.UseStartup<Startup>()
.Build();

host.Run();
}
}

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

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 al que se llama en la canalización de
solicitudes.
Para obtener más información, vea Inicio de la aplicación en ASP.NET Core.

Raíz del contenido


La raíz del contenido es la ruta de acceso base a cualquier contenido que usa la aplicación, como Razor Pages, las
vistas de MVC y los recursos estáticos. De forma predeterminada, la raíz del contenido es la misma ubicación 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 lo prefiere, puede
reemplazar el contenedor 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, vea Inserción de dependencias en ASP.NET Core.

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 las operaciones asincrónicas en un HttpContext y, después, invoca el siguiente
software intermedio de la canalización o finaliza la solicitud.
Normalmente, se agrega un componente de software intermedio denominado "XYZ" a la canalización al invocar
a un método de extensión UseXYZ en el método Configure .
ASP.NET Core incluye una gran variedad de software intermedio integrado. Además, puede escribir su propio
software intermedio personalizado. La interfaz web abierta para .NET (OWIN ) , que permite desacoplar las
aplicaciones web de servidores web, es compatible con las aplicaciones de ASP.NET Core.
Para obtener más información, consulte Middleware de ASP.NET Core y Interfaz web abierta para .NET (OWIN )
con ASP.NET Core.

Inicio de solicitudes HTTP


IHttpClientFactory está disponible para acceder a instancias de HttpClient con el fin de realizar solicitudes HTTP.
Para obtener más información, 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 una variable de entorno, un archivo de configuración o un argumento de línea de comandos.
Para obtener más información, vea Usar varios entornos en ASP.NET Core.

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, vea Hospedaje en ASP.NET Core.

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 suele
ejecutarse tras un servidor web de producción, como IIS o Nginx, en una configuración proxy invertida. Kestrel
también puede ejecutarse como servidor perimetral expuesto directamente a Internet en ASP.NET Core 2.0 o
versiones posteriores.
Para obtener más información, vea Implementaciones de servidores web en ASP.NET Core.

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 gran variedad de
formatos de archivo (XML, JSON, INI), variables de entorno y argumentos de línea de comandos. También puede
escribir sus propios proveedores de configuración personalizados.
Para obtener más información, vea Configuración en ASP.NET Core.

Registro
ASP.NET Core es compatible con una API de registro que funciona con una gran 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.
Para obtener más información, vea Registro en ASP.NET Core.

Control de errores
ASP.NET Core tiene escenarios integrados para controlar los errores en las aplicaciones, incluidas 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 obtener más información, vea Controlar errores en ASP.NET Core.

Enrutamiento
ASP.NET Core ofrece casos para el enrutamiento de solicitudes de aplicación a los controladores de ruta.
Para obtener más información, vea Enrutamiento en ASP.NET Core.

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 obtener más información, vea Proveedores de archivo en ASP.NET Core.
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, vea Archivos estáticos en ASP.NET Core.

Estado de sesión y aplicación


ASP.NET Core ofrece varios enfoques para conservar el estado de sesión y aplicación mientras el usuario
examina una aplicación web.
Para obtener más información, vea Estado de sesión y aplicación en ASP.NET Core.

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.
Además, proporciona servicios y software intermedio para la localización de contenido en diferentes idiomas y
referencias culturales.
Para obtener más información, vea Globalización y localización en ASP.NET Core.

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 obtener más información, vea Características de solicitud de ASP.NET Core.

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 obtener más información, vea Tareas en segundo plano con servicios hospedados en ASP.NET Core.

Acceso a HttpContext
HttpContext está disponible automáticamente al procesar las solicitudes con Razor Pages y MVC. En los casos
en los que HttpContext no esté disponible, podrá acceder a HttpContext a través de la interfaz de
IHttpContextAccessor y su implementación predeterminada, HttpContextAccessor.
Para obtener más información, vea Acceso a HttpContext en ASP.NET Core.

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 casos de socket web.
Para obtener más información, vea Compatibilidad con WebSockets en ASP.NET Core.

Metapaquete Microsoft.AspNetCore
El metapaquete Microsoft.AspNetCore.App simplifica la administración de metapaquetes.
Para obtener más información, vea Metapaquete Microsoft.AspNetCore.App para ASP.NET Core 2.1 y versiones
posteriores.

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 obtener más información, vea Metapaquete Microsoft.AspNetCore.All para ASP.NET Core 2.0.

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 obtener más información sobre cómo elegir entre ASP.NET Core y ASP.NET, vea Elegir entre ASP.NET y
ASP.NET Core.
Inicio de la aplicación en ASP.NET Core
12/09/2018 • 12 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)
{
CreateWebHostBuilder(args).Build().Run();
}

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>


WebHost.CreateDefaultBuilder(args)
.UseStartup<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 . Después, los servicios de la aplicación
y el host están disponibles en Configure y en toda la aplicación.
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 leer la configuración.
ILoggerFactory para crear un registrador en Startup.ConfigureServices .

public class Startup


{
private readonly IHostingEnvironment _env;
private readonly IConfiguration _config;
private readonly ILoggerFactory _loggerFactory;

public Startup(IHostingEnvironment env, IConfiguration config,


ILoggerFactory loggerFactory)
{
_env = env;
_config = config;
_loggerFactory = loggerFactory;
}

public void ConfigureServices(IServiceCollection services)


{
var logger = _loggerFactory.CreateLogger<Startup>();

if (_env.IsDevelopment())
{
// Development service configuration

logger.LogInformation("Development environment");
}
else
{
// Non-development service configuration

logger.LogInformation($"Environment: {_env.EnvironmentName}");
}

// Configuration is available during startup.


// Examples:
// _config["key"]
// _config["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 distintos entornos (por
ejemplo, StartupDevelopment ) y la clase Startup 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.
El patrón habitual consiste en llamar a todos los métodos Add{Service} y, luego, a todos los métodos
services.Configure{Service} . Por ejemplo, consulte Configurar servicios de identidad.
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>();
}

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. El hospedaje crea un elemento IApplicationBuilder y lo pasa
directamente a Configure .
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 Core 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)


{
CreateWebHostBuilder(args).Build().Run();
}

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>


WebHost.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((hostingContext, config) =>
{
HostingEnvironment = hostingContext.HostingEnvironment;
Configuration = config.Build();
})
.ConfigureServices(services =>
{
services.AddMvc();
})
.Configure(app =>
{
var loggerFactory = app.ApplicationServices
.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger<Program>();
logger.LogInformation("Logged in Configure");

if (HostingEnvironment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
}

// Configuration is available during startup. Examples:


// Configuration["key"]
// Configuration["subsection:suboption1"]

app.UseMvcWithDefaultRoute();
app.UseStaticFiles();
});
}

Extensión del inicio con filtros de inicio


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

IStartupFilterestá registrado en el contenedor de servicios en IWebHostBuilder.ConfigureServices para


demostrar cómo el filtro de inicio aumenta Startup desde fuera de la clase Startup :

WebHost.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
services.AddTransient<IStartupFilter,
RequestSetOptionsStartupFilter>();
})
.UseStartup<Startup>()
.Build();

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 desde 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
Hospedaje en ASP.NET Core
Usar varios entornos en ASP.NET Core
Middleware de ASP.NET Core
Registro en ASP.NET Core
Configuración en ASP.NET Core
Inserción de dependencias en
ASP.NET Core
31/08/2018 • 31 minutes to read • Edit Online

Por Steve Smith, Scott Addie y Luke Latham


ASP.NET Core admite el patrón de diseño de software de inserción de
dependencias (DI), que es una técnica para conseguir la inversión de control
(IoC ) entre clases y sus dependencias.
Para más información específica sobre la inserción de dependencias en los
controladores MVC, vea Inserción de dependencias en controladores en
ASP.NET Core.
Vea o descargue el código de ejemplo (cómo descargarlo)

Información general sobre la inserción de


dependencias
Una dependencia es cualquier objeto requerido por otro objeto. Examine la
siguiente clase MyDependency con un método WriteMessage del que
dependen otras clases de una aplicación:

public class MyDependency


{
public MyDependency()
{
}

public Task WriteMessage(string message)


{
Console.WriteLine(
$"MyDependency.WriteMessage called. Message: {message}");

return Task.FromResult(0);
}
}

Se puede crear una instancia de la clase MyDependency para hacer que el


método WriteMessage esté disponible para una clase. La clase MyDependency
es una dependencia de la clase IndexModel :

public class IndexModel : PageModel


{
MyDependency _dependency = new MyDependency();

public async Task OnGetAsync()


{
await _dependency.WriteMessage(
"IndexModel.OnGetAsync created this message.");
}
}
Se puede crear una instancia de la clase MyDependency para hacer que el
método WriteMessage esté disponible para una clase. La clase MyDependency
es una dependencia de la clase HomeController :

public class HomeController : Controller


{
MyDependency _dependency = new MyDependency();

public async Task<IActionResult> Index()


{
await _dependency.WriteMessage(
"HomeController.Index created this message.");

return View();
}
}

La clase crea y depende directamente de la instancia MyDependency . Las


dependencias de código (como en el ejemplo anterior) son problemáticas y
deben evitarse por las siguientes razones:
Para reemplazar MyDependency con una implementación diferente, se
debe modificar la clase.
Si MyDependency tiene dependencias, deben configurarse según la clase.
En un proyecto grande con varias clases que dependen de MyDependency ,
el código de configuración se dispersa por la aplicación.
Esta implementación es difícil para realizar pruebas unitarias. La
aplicación debe usar una clase MyDependency como boceto o código
auxiliar, que no es posible con este enfoque.
La inserción de dependencias aborda estos problemas mediante:
El uso de una interfaz para abstraer la implementación de dependencias.
Registro de la dependencia en un contenedor de servicios. ASP.NET
Core proporciona un contenedor de servicios integrado,
IServiceProvider. Los servicios se registran en el método
Startup.ConfigureServices de la aplicación.
Inserción del servicio en el constructor de la clase en la que se usa. El
marco de trabajo asume la responsabilidad de crear una instancia de la
dependencia y de desecharla cuando ya no es necesaria.
En la aplicación de ejemplo, la interfaz IMyDependency define un método que
el servicio proporciona a la aplicación:

public interface IMyDependency


{
Task WriteMessage(string message);
}

public interface IMyDependency


{
Task WriteMessage(string message);
}

Esta interfaz se implementa mediante un tipo concreto, MyDependency :


public class MyDependency : IMyDependency
{
private readonly ILogger<MyDependency> _logger;

public MyDependency(ILogger<MyDependency> logger)


{
_logger = logger;
}

public Task WriteMessage(string message)


{
_logger.LogInformation(
"MyDependency.WriteMessage called. Message: {MESSAGE}",
message);

return Task.FromResult(0);
}
}

public class MyDependency : IMyDependency


{
private readonly ILogger<MyDependency> _logger;

public MyDependency(ILogger<MyDependency> logger)


{
_logger = logger;
}

public Task WriteMessage(string message)


{
_logger.LogInformation(
"MyDependency.WriteMessage called. Message: {MESSAGE}",
message);

return Task.FromResult(0);
}
}

MyDependency solicita una instancia de ILogger<TCategoryName> en su


constructor. No es raro usar la inserción de dependencias de forma
encadenada. Cada dependencia solicitada a su vez solicita sus propias
dependencias. El contenedor resuelve las dependencias del gráfico y
devuelve el servicio totalmente resuelto. El conjunto colectivo de
dependencias que deben resolverse suele denominarse árbol de
dependencias, gráfico de dependencias o gráfico de objetos.
IMyDependency y ILogger<TCategoryName> deben estar registrados en el
contenedor de servicios. IMyDependency está registrado en
Startup.ConfigureServices . ILogger<TCategoryName> está registrado en la
infraestructura de abstracciones de registros, por lo que se trata de un
servicio proporcionado por el marco de trabajo registrado de forma
predeterminada por el marco de trabajo.
En la aplicación de ejemplo, el servicio IMyDependency está registrado con el
tipo concreto MyDependency . El registro abarca la duración del servicio como
la duración de una única solicitud. Las duraciones del servicio se describen
más adelante en este tema.
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});

services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2
_1);

services.AddScoped<IMyDependency, MyDependency>();
services.AddTransient<IOperationTransient, Operation>();
services.AddScoped<IOperationScoped, Operation>();
services.AddSingleton<IOperationSingleton, Operation>();
services.AddSingleton<IOperationSingletonInstance>(new
Operation(Guid.Empty));

// OperationService depends on each of the other Operation types.


services.AddTransient<OperationService, OperationService>();
}

public void ConfigureServices(IServiceCollection services)


{
services.AddMvc();

services.AddScoped<IMyDependency, MyDependency>();
services.AddTransient<IOperationTransient, Operation>();
services.AddScoped<IOperationScoped, Operation>();
services.AddSingleton<IOperationSingleton, Operation>();
services.AddSingleton<IOperationSingletonInstance>(new
Operation(Guid.Empty));

// OperationService depends on each of the other Operation types.


services.AddTransient<OperationService, OperationService>();
}

NOTE
Cada método de extensión services.Add{SERVICE_NAME} agrega servicios (y
potencialmente los configura). Por ejemplo, services.AddMvc() agrega los
servicios que Razor Pages y MVC requieren. Se recomienda que las aplicaciones
sigan esta convención. Coloque los métodos de extensión en el espacio de
nombres Microsoft.Extensions.DependencyInjection para encapsular grupos de
registros del servicio.

Si el constructor de un servicio requiere un primitivo, como string , se


puede insertar mediante la configuración o el patrón de opciones.
public class MyDependency : IMyDependency
{
public MyDependency(IConfiguration config)
{
var myStringValue = config["MyStringKey"];

// Use myStringValue
}

...
}

Se solicita una instancia del servicio mediante el constructor de una clase, en


la que se usa el servicio y se asigna a un campo privado. El campo de utiliza
para acceder al servicio, según sea necesario en la clase.
En la aplicación de ejemplo, la instancia IMyDependency se solicita y usa para
llamar al método WriteMessage del servicio:

public class IndexModel : PageModel


{
private readonly IMyDependency _myDependency;

public IndexModel(
IMyDependency myDependency,
OperationService operationService,
IOperationTransient transientOperation,
IOperationScoped scopedOperation,
IOperationSingleton singletonOperation,
IOperationSingletonInstance singletonInstanceOperation)
{
_myDependency = myDependency;
OperationService = operationService;
TransientOperation = transientOperation;
ScopedOperation = scopedOperation;
SingletonOperation = singletonOperation;
SingletonInstanceOperation = singletonInstanceOperation;
}

public OperationService OperationService { get; }


public IOperationTransient TransientOperation { get; }
public IOperationScoped ScopedOperation { get; }
public IOperationSingleton SingletonOperation { get; }
public IOperationSingletonInstance SingletonInstanceOperation { get;
}

public async Task OnGetAsync()


{
await _myDependency.WriteMessage(
"IndexModel.OnGetAsync created this message.");
}
}
public class MyDependencyController : Controller
{
private readonly IMyDependency _myDependency;

public MyDependencyController(IMyDependency myDependency)


{
_myDependency = myDependency;
}

// GET: /mydependency/
public async Task<IActionResult> Index()
{
await _myDependency.WriteMessage(
"MyDependencyController.Index created this message.");

return View();
}
}

Servicios proporcionados por el marco de


trabajo
El método Startup.ConfigureServices se encarga de definir los servicios que
la aplicación usa, 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.Builder.IA Transitorio
pplicationBuilderFactory

Microsoft.AspNetCore.Hosting.IApplicati Singleton
onLifetime

Microsoft.AspNetCore.Hosting.IHostingE Singleton
nvironment

Microsoft.AspNetCore.Hosting.IStartup Singleton

Microsoft.AspNetCore.Hosting.IStartupFi Transitorio
lter

Microsoft.AspNetCore.Hosting.Server.ISe Singleton
rver

Microsoft.AspNetCore.Http.IHttpContext Transitorio
Factory

Microsoft.Extensions.Logging.ILogger<T Singleton
>

Microsoft.Extensions.Logging.ILoggerFac Singleton
tory
TIPO DE SERVICIO PERÍODO DE DURACIÓN

Microsoft.Extensions.ObjectPool.ObjectP Singleton
oolProvider

Microsoft.Extensions.Options.IConfigure Transitorio
Options<T>

Microsoft.Extensions.Options.IOptions< Singleton
T>

System.Diagnostics.DiagnosticSource Singleton

System.Diagnostics.DiagnosticListener Singleton

Cuando un método de extensión de la colección de servicio está disponible


para registrar un servicio (y sus servicios dependientes, si es necesario), la
convención consiste en usar un solo método de extensión
Add{SERVICE_NAME} para registrar todos los servicios requeridos por dicho
servicio. El código siguiente es un ejemplo de cómo agregar servicios
adicionales al contenedor mediante los métodos de extensión
AddDbContext, AddIdentity y AddMvc:

public void ConfigureServices(IServiceCollection services)


{
services.AddDbContext<ApplicationDbContext>(options =>

options.UseSqlServer(Configuration.GetConnectionString("DefaultConnectio
n")));

services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();

services.AddMvc();
}

Para más información, vea la clase ServiceCollection en la documentación


de la API.

Duraciones de servicios
Elija una duración adecuada para cada servicio registrado. Los servicios de
ASP.NET Core 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. Para obtener
más información, vea Middleware de ASP.NET Core.

Singleton
Los servicios con duración Singleton se crean la primera vez que se
solicitan, o bien cuando se ejecuta ConfigureServices y se especifica una
instancia con el registro del servicio. Cada solicitud posterior usa 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. No implemente el patrón de diseño de singleton y proporcione
el código de usuario para administrar la duración del objeto en la clase.

WARNING
Es peligroso resolver un servicio con ámbito desde un singleton. Puede dar lugar
a que el servicio adopte un estado incorrecto al procesar solicitudes posteriores.

Comportamiento de inserción de constructor


Los servicios se pueden resolver mediante dos mecanismos:
IServiceProvider
ActivatorUtilities – Permite la creación de objetos sin registrar el servicio
en el contenedor de inserción de dependencias. ActivatorUtilities se
utiliza con abstracciones orientadas al usuario, como aplicaciones
auxiliares de etiquetas, controladores MVC, concentradores SignalR y
enlazadores de modelos.
Los constructores pueden aceptar argumentos que no se proporcionan
mediante la inserción de dependencias, pero los argumentos deben asignar
valores predeterminados.
Cuando se resuelven los servicios mediante IServiceProvider o
ActivatorUtilities , la inserción del constructor requiere un constructor
público.
Cuando se resuelven los servicios mediante ActivatorUtilities , la
inserción del constructor requiere que exista solo un constructor aplicable.
Se admiten las sobrecargas de constructor, pero solo puede existir una
sobrecarga cuyos argumentos pueda cumplir la inserción de dependencias.

Contextos de Entity Framework


Es necesario agregar contextos de Entity Framework al contenedor de
servicios mediante la duración con ámbito. Esto se controla
automáticamente con una llamada al método AddDbContext al registrar el
contexto de la base de datos. Los servicios que usan el contexto de la base
de datos deben usar también la duración con ámbito.
Opciones de registro y duración
Para mostrar la diferencia entre la duración y las opciones de registro,
considere las siguientes interfaces que representan tareas como una
operación con un identificador único, OperationId . Según cómo esté
configurada la duración de un servicio de operaciones para las interfaces
siguientes, el contenedor proporciona la misma instancia del servicio u otra
distinta cuando así lo solicita la clase:

public interface IOperation


{
Guid OperationId { get; }
}

public interface IOperationTransient : IOperation


{
}

public interface IOperationScoped : IOperation


{
}

public interface IOperationSingleton : IOperation


{
}

public interface IOperationSingletonInstance : IOperation


{
}

public interface IOperation


{
Guid OperationId { get; }
}

public interface IOperationTransient : IOperation


{
}

public interface IOperationScoped : IOperation


{
}

public interface IOperationSingleton : IOperation


{
}

public interface IOperationSingletonInstance : IOperation


{
}

Las interfaces se implementan en la clase Operation . El constructor


Operation genera un GUID en caso de que no se proporcione uno:
public class Operation : IOperationTransient,
IOperationScoped,
IOperationSingleton,
IOperationSingletonInstance
{
public Operation() : this(Guid.NewGuid())
{
}

public Operation(Guid id)


{
OperationId = id;
}

public Guid OperationId { get; private set; }


}

public class Operation : IOperationTransient,


IOperationScoped,
IOperationSingleton,
IOperationSingletonInstance
{
public Operation() : this(Guid.NewGuid())
{
}

public Operation(Guid id)


{
OperationId = id;
}

public Guid OperationId { get; private set; }


}

Se registra una instancia de OperationService que depende de cada uno de


los demás tipos Operation . Cuando OperationService se solicita con la
inserción de dependencias, recibe una instancia nueva de cada servicio o
una instancia existente en función de la duración de los servicios
dependientes.
Si se crean servicios transitorios cuando se solicitan, el elemento
OperationsId del servicio IOperationTransient varía del objeto
OperationsId de OperationService . OperationService recibe una
instancia nueva de la clase IOperationTransient . La nueva instancia
produce un objeto OperationsId diferente.
Si se crean servicios con ámbito por cada solicitud, el objeto
OperationsId del servicio IOperationScoped es el mismo que para
OperationService dentro de la solicitud. Entre las solicitudes, ambos
servicios comparten un valor OperationsId diferente.
Si se crean servicios singleton y servicios de instancia singleton una vez
y se usan en todas las solicitudes y servicios, el objeto OperationsId es
constante en todas las solicitudes de servicio.
public class OperationService
{
public OperationService(
IOperationTransient transientOperation,
IOperationScoped scopedOperation,
IOperationSingleton singletonOperation,
IOperationSingletonInstance instanceOperation)
{
TransientOperation = transientOperation;
ScopedOperation = scopedOperation;
SingletonOperation = singletonOperation;
SingletonInstanceOperation = instanceOperation;
}

public IOperationTransient TransientOperation { get; }


public IOperationScoped ScopedOperation { get; }
public IOperationSingleton SingletonOperation { get; }
public IOperationSingletonInstance SingletonInstanceOperation { get;
}
}

public class OperationService


{
public OperationService(IOperationTransient transientOperation,
IOperationScoped scopedOperation,
IOperationSingleton singletonOperation,
IOperationSingletonInstance instanceOperation)
{
TransientOperation = transientOperation;
ScopedOperation = scopedOperation;
SingletonOperation = singletonOperation;
SingletonInstanceOperation = instanceOperation;
}

public IOperationTransient TransientOperation { get; }


public IOperationScoped ScopedOperation { get; }
public IOperationSingleton SingletonOperation { get; }
public IOperationSingletonInstance SingletonInstanceOperation { get;
}
}

En Startup.ConfigureServices , cada tipo se agrega al contenedor según su


duración con nombre:
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});

services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2
_1);

services.AddScoped<IMyDependency, MyDependency>();
services.AddTransient<IOperationTransient, Operation>();
services.AddScoped<IOperationScoped, Operation>();
services.AddSingleton<IOperationSingleton, Operation>();
services.AddSingleton<IOperationSingletonInstance>(new
Operation(Guid.Empty));

// OperationService depends on each of the other Operation types.


services.AddTransient<OperationService, OperationService>();
}

public void ConfigureServices(IServiceCollection services)


{
services.AddMvc();

services.AddScoped<IMyDependency, MyDependency>();
services.AddTransient<IOperationTransient, Operation>();
services.AddScoped<IOperationScoped, Operation>();
services.AddSingleton<IOperationSingleton, Operation>();
services.AddSingleton<IOperationSingletonInstance>(new
Operation(Guid.Empty));

// OperationService depends on each of the other Operation types.


services.AddTransient<OperationService, OperationService>();
}

El servicio IOperationSingletonInstance usa una instancia específica con un


identificador conocido de Guid.Empty . Resulta evidente identificar cuándo
este tipo está en uso (su GUID es todo ceros).
La aplicación de ejemplo muestra las duraciones de los objetos dentro y
entre las solicitudes individuales. El objeto IndexModel de la aplicación de
ejemplo solicita cada tipo de IOperation y OperationService . Después, la
página muestra todos los valores OperationId del servicio y la clase del
modelo de página mediante las asignaciones de propiedades:
public class IndexModel : PageModel
{
private readonly IMyDependency _myDependency;

public IndexModel(
IMyDependency myDependency,
OperationService operationService,
IOperationTransient transientOperation,
IOperationScoped scopedOperation,
IOperationSingleton singletonOperation,
IOperationSingletonInstance singletonInstanceOperation)
{
_myDependency = myDependency;
OperationService = operationService;
TransientOperation = transientOperation;
ScopedOperation = scopedOperation;
SingletonOperation = singletonOperation;
SingletonInstanceOperation = singletonInstanceOperation;
}

public OperationService OperationService { get; }


public IOperationTransient TransientOperation { get; }
public IOperationScoped ScopedOperation { get; }
public IOperationSingleton SingletonOperation { get; }
public IOperationSingletonInstance SingletonInstanceOperation { get;
}

public async Task OnGetAsync()


{
await _myDependency.WriteMessage(
"IndexModel.OnGetAsync created this message.");
}
}

La aplicación de ejemplo muestra las duraciones de los objetos dentro y


entre las solicitudes individuales. La aplicación de ejemplo incluye un valor
OperationsController que solicita cada tipo de IOperation y
OperationService . La acción Index establece el servicio en ViewBag para
mostrar los valores OperationId del servicio:
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();
}
}

Las dos salidas siguientes muestran el resultado de dos solicitudes:


Primera solicitud:
Operaciones del controlador:
Transient: d233e165-f417-469b-a866-1cf1935d2518
Scoped: 5d997e2d-55f5-4a64-8388-51c4e3a1ad19
Singleton: 01271bc1-9e31-48e7-8f7c-7261b040ded9
Instance: 00000000-0000-0000-0000-000000000000
Operaciones OperationService :
Transient: c6b049eb-1318-4e31-90f1-eb2dd849ff64
Scoped: 5d997e2d-55f5-4a64-8388-51c4e3a1ad19
Singleton: 01271bc1-9e31-48e7-8f7c-7261b040ded9
Instance: 00000000-0000-0000-0000-000000000000
Segunda solicitud:
Operaciones del controlador:
Transient: b63bd538-0a37-4ff1-90ba-081c5138dda0
Scoped: 31e820c5-4834-4d22-83fc-a60118acb9f4
Singleton: 01271bc1-9e31-48e7-8f7c-7261b040ded9
Instance: 00000000-0000-0000-0000-000000000000
Operaciones OperationService :
Transitorio: c4cbacb8-36a2-436d-81c8-8c1b78808aaf
Scoped: 31e820c5-4834-4d22-83fc-a60118acb9f4
Singleton: 01271bc1-9e31-48e7-8f7c-7261b040ded9
Instance: 00000000-0000-0000-0000-000000000000
Observe cuál de los valores OperationId varía dentro de una solicitud y
entre solicitudes:
Los objetos Transient siempre son diferentes. Tenga en cuenta que el
valor OperationId transitorio de la primera y segunda solicitud varía
tanto para las operaciones OperationService como entre solicitudes. Se
proporciona una nueva instancia para cada servicio y solicitud.
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 Operation en
ConfigureServices .

Llamada a servicios desde main


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 = CreateWebHostBuilder(args).Build();

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, 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 Host web de ASP.NET Core.

Servicios de solicitud
Los servicios disponibles en una solicitud de ASP.NET Core desde
HttpContext se exponen mediante la colección
HttpContext.RequestServices.
Los servicios de solicitud representan los servicios configurados y
solicitados 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, la aplicación no debe usar estas propiedades directamente.


En su lugar, solicite los tipos que las clases requieren mediante el
constructor de clases y permita que el marco de trabajo inserte las
dependencias. Esto da como resultado clases más fáciles de probar (vea
Pruebas y depuración).

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
Los procedimientos recomendados son:
Diseñar servicios para usar la inserción de dependencias a fin de obtener
sus dependencias.
Evitar llamadas a métodos estáticas y con estado (una práctica conocida
como estática).
Evitar la creación directa de instancias de clases dependientes dentro de
los servicios. La creación directa de instancias se acopla al código de una
implementación particular.
Si sigue los principios SOLID del diseño orientado a objetos, las clases de
aplicaciones tenderán de forma natural a ser pequeñas, estarán
correctamente factorizadas y podrán probarse fácilmente.
Si una clase parece tener demasiadas dependencias insertadas, suele indicar
que la clase tiene demasiadas responsabilidades y que esto infringe el
principio de responsabilidad única (SRP ). Trate de mover algunas de las
responsabilidades de la clase a una nueva para intentar refactorizarla. Tenga
en cuenta que las clases del modelo de página de Razor Pages y las clases
de controlador MVC deben centrarse en aspectos de la interfaz de usuario.
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.
Eliminación de servicios
El contenedor llama a Dispose para los tipos IDisposable que crea. Si se
agrega una instancia al contenedor por código de usuario, no se elimina
automáticamente.

// Services that 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)


{
// The container creates the following instances and disposes them
automatically:
services.AddScoped<Service1>();
services.AddSingleton<Service2>();
services.AddSingleton<ISomeService>(sp => new
SomeServiceImplementation());

// The container doesn't create the following instances, so it


doesn't dispose of
// the instances automatically:
services.AddSingleton<Service3>(new Service3());
services.AddSingleton(new Service3());
}

NOTE
En ASP.NET Core 1.0, el contenedor llama 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 del marco de trabajo y de la mayoría de las aplicaciones de
consumidor. Se recomienda usar el contenedor integrado a menos que se
necesite una característica específica no admitida por el contenedor. Algunas
de las características admitidas en contenedores de terceros no se incluyen
en el contenedor integrado:
Inserción de propiedades
Inserción basada en nombres
Contenedores secundarios
Administración personalizada del ciclo de vida
Compatibilidad con Func<T> para la inicialización diferida
Vea el archivo readme.md de inserción de dependencias para obtener una
lista de algunos de los contenedores que admiten adaptadores.
En el ejemplo siguiente se reemplaza el contenedor integrado por Autofac:
Instale los paquetes de contenedor adecuados:
Autofac
Autofac.Extensions.DependencyInjection
Configure el contenedor en Startup.ConfigureServices y devuelva
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);
}

Para usar un contenedor de terceros, Startup.ConfigureServices


debe devolver IServiceProvider .
Configure Autofac en DefaultModule :

public class DefaultModule : Module


{
protected override void Load(ContainerBuilder builder)
{
builder.RegisterType<CharacterRepository>
().As<ICharacterRepository>();
}
}

En tiempo de ejecución, se usa Autofac para resolver tipos e insertar


dependencias. Para más información sobre el uso de Autofac con ASP.NET
Core, vea la documentación sobre Autofac.
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.
El patrón de diseño Factory Method de un servicio único, como el segundo
argumento para AddSingleton<TService>(IServiceCollection,
Func<IServiceProvider,TService>), no necesita ser seguro para
subprocesos. Al igual que un constructor de tipos ( static ), se garantiza que
se le llame una vez mediante un único subproceso.

Recomendaciones
Cuando trabaje con la inserción de dependencias, tenga en cuenta las
recomendaciones siguientes:
Evite almacenar datos y configuraciones directamente en el
contenedor de servicios. 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
mediante la inserción de dependencias, si es posible.
Evite el acceso estático a servicios (por ejemplo, escribiendo de forma
estática IApplicationBuilder.ApplicationServices para usarlo en otro
lugar).
Evite usar el patrón del localizador de servicios (por ejemplo,
IServiceProvider.GetService).
Evite el acceso estático a HttpContext (por ejemplo,
IHttpContextAccessor.HttpContext).
Al igual que sucede con todas las recomendaciones, podría verse en una
situación que le obligue a ignorar alguna de ellas. Las excepciones son poco
frecuentes; principalmente en casos especiales dentro 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 de ASP.NET Core
Inserción de dependencias en controladores en ASP.NET Core
Inserción de dependencias en controladores de requisitos en ASP.NET
Core
Patrón de repositorio con ASP.NET Core
Inicio de la aplicación en ASP.NET Core
Probar, depurar y solucionar problemas en ASP.NET Core
Activación de middleware basada en Factory en ASP.NET Core
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 (Martin Fowler)
La unión como novedad ("unir" código a una implementación particular)
Middleware de ASP.NET Core
28/08/2018 • 19 minutes to read • Edit Online

Por Rick Anderson y Steve Smith


El software intermedio es un software que se ensambla en 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 se conocen como software
intermedio o componentes de software intermedio. Cada componente de software intermedio de la canalización de
solicitudes es responsable de invocar el siguiente componente de la canalización o de cortocircuitar la canalización,
en caso de ser necesario.
En Migración de módulos y controladores HTTP a middleware de ASP.NET Core 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 software
intermedio.

Creación de una canalización de software intermedio con


IApplicationBuilder
La canalización de solicitudes de ASP.NET Core consiste en una secuencia de delegados de solicitud a los que se
llama de uno en uno. En el siguiente diagrama se muestra este concepto. El subproceso 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 software intermedio
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 se llaman al principio de la canalización para que puedan
capturar las excepciones que se produzcan 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.

public class Startup


{
public void Configure(IApplicationBuilder app)
{
app.Run(async context =>
{
await context.Response.WriteAsync("Hello, World!");
});
}
}

El primer delegado de Run finaliza la canalización.


Encadene varios delegados de solicitudes con Use. El parámetro next representa el siguiente delegado de la
canalización. Si no llama al siguiente parámetro, puede cortocircuitar la canalización. Normalmente, puede realizar
acciones antes y después del siguiente delegado, tal como se muestra en el ejemplo siguiente:

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. Si se modifica HttpResponse después de haber
iniciado la respuesta, se producirá una excepción. Por ejemplo, se producirá una excepción al realizar cambios tales como el
establecimiento de encabezados o el código. 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.
HasStarted es una sugerencia útil para indicar si se han enviado los encabezados o se han realizado escrituras en el cuerpo.

Orden
El orden en el que se agregan los componentes de software intermedio en el método Startup.Configure define el
orden en el que se invocarán los componentes de software intermedio en las solicitudes y el orden inverso de la
respuesta. Por motivos de seguridad, rendimiento y funcionalidad, el orden es básico.
El método Configure siguiente agrega los siguientes componentes de software intermedio:
1. Control de errores y excepciones
2. Servidor de archivos estáticos
3. Autenticación
4. MVC

public void Configure(IApplicationBuilder app)


{
if (env.IsDevelopment())
{
// When the app runs in the Development environment:
// Use the Developer Exception Page to report app runtime errors.
// Use the Database Error Page to report database runtime errors.
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
// When the app doesn't run in the Development environment:
// Enable the Exception Handler Middleware to catch exceptions
// thrown in the following middlewares.
// Use the HTTP Strict Transport Security Protocol (HSTS)
// Middleware.
app.UseExceptionHandler("/Error");
app.UseHsts();
}

// Use HTTPS Redirection Middleware to redirect HTTP requests to HTTPS.


app.UseHttpsRedirection();

// Return static files and end the pipeline.


app.UseStaticFiles();

// Use Cookie Policy Middleware to conform to EU General Data


// Protection Regulation (GDPR) regulations.
app.UseCookiePolicy();

// Authenticate before the user accesses secure resources.


app.UseAuthentication();

// Add MVC to the request pipeline.


app.UseMvc();
}

public void Configure(IApplicationBuilder app)


{
// Enable the Exception Handler Middleware to catch exceptions
// thrown in the following middlewares.
app.UseExceptionHandler("/Home/Error");

// Return static files and end the pipeline.


app.UseStaticFiles();

// Authenticate before you access secure resources.


app.UseIdentity();

// Add MVC to the request pipeline.


app.UseMvcWithDefaultRoute();
}
En el código de ejemplo anterior, cada método de extensión de software intermedio se expone en
IApplicationBuilder a través del espacio de nombres de Microsoft.AspNetCore.Builder.
UseExceptionHandler es el primer componente de software intermedio que se agrega a la canalización. Por lo
tanto, el software intermedio del controlador de excepciones detectará las excepciones que se produzcan en las
llamadas posteriores.
El software intermedio 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 software intermedio no
proporciona comprobaciones de autorización. Los archivos que proporciona, incluidos los de wwwroot, están
disponibles de forma pública. Para obtener más información sobre cómo proteger este tipo de archivos, vea
Archivos estáticos en ASP.NET Core.
Si el software intermedio de archivos estáticos no controla la solicitud, se pasará al software intermedio de
autenticación (UseAuthentication), que realizará la autenticación. Este software intermedio no cortocircuita las
solicitudes sin autenticación. Aunque autentique solicitudes, la autorización (y también el rechazo) se producirán
después de que MVC seleccione una página de Razor o un control y una acción de MVC concretos.
Si el software intermedio de archivos estáticos no controla la solicitud, se pasará al software intermedio de
identidad (UseIdentity), que realizará la autenticación. Este middleware no cortocircuita las solicitudes sin
autenticación. Aunque autentique solicitudes, la autorización (y también el rechazo) se producirán después de que
MVC seleccione un control y una acción concretos.
En el ejemplo siguiente se muestra un orden de software intermedio en el que el software intermedio de archivos
estáticos controla las solicitudes de archivos estáticos antes del software intermedio de compresión de respuestas.
Los archivos estáticos no se comprimen en este orden de software intermedio. Las respuestas de MVC de
UseMvcWithDefaultRoute se pueden comprimir.

public void Configure(IApplicationBuilder app)


{
// Static files not compressed by Static Files Middleware.
app.UseStaticFiles();
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" processing
});
level1App.Map("/level2b", level2BApp => {
// "/level1/level2b" processing
});
});

Map puede hacer coincidir varios segmentos a la vez:


public class Startup
{
private static void HandleMultiSeg(IApplicationBuilder app)
{
app.Run(async context =>
{
await context.Response.WriteAsync("Map multiple segments.");
});
}

public void Configure(IApplicationBuilder app)


{
app.Map("/map1/seg1", HandleMultiSeg);

app.Run(async context =>


{
await context.Response.WriteAsync("Hello from non-Map delegate.");
});
}
}

Middleware integrado
ASP.NET Core incluye los componentes de software intermedio siguientes. En la columna Orden se proporcionan
notas sobre la ubicación del software intermedio en la solicitud de canalización, así como las condiciones con las
que podría finalizar la solicitud y evitar que otro software intermedio procese una solicitud.

SOFTWARE INTERMEDIO DESCRIPCIÓN 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 HTTP Antes de los componentes que
a 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).
SOFTWARE INTERMEDIO DESCRIPCIÓN ORDEN

MVC Procesa la solicitudes con MVC o Razor Si hay una solicitud que coincida con
Pages (ASP.NET Core 2.0 o versiones una ruta, será final.
posteriores).

OWIN Puede interoperar con aplicaciones, Si el software intermedio de OWIN


servidores y software intermedio procesa completamente la solicitud, será
basados en OWIN. final.

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.

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 Si hay una solicitud que coincida con un
proporción de archivos estáticos y la archivo, será final.
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 software intermedio


El middleware normalmente está encapsulado en una clase y se expone con un método de extensión. Use el
siguiente software intermedio 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}");
});

}
}

El código de ejemplo anterior se usa para mostrar la creación de un componente de software intermedio. Para
obtener más información sobre la compatibilidad con la localización integrada de ASP.NET Core, vea Globalización
y localización en 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 async 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


await _next(context);
}
}
}

El nombre del software intermedio del método 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 software intermedio 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 Startup.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. Si necesita compartir servicios
con software intermedio en una solicitud, vea la sección Dependencias bajo solicitud.
Los componentes de software intermedio pueden resolver sus dependencias de una inserción de dependencias (DI)
mediante parámetros del constructor. UseMiddleware<T> también puede aceptar parámetros adicionales
directamente.
Dependencias bajo solicitud
Dado que el software intermedio se construye al inicio de la aplicación y no bajo solicitud, los servicios de duración
con ámbito que usan los constructores de software intermedio 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 la inserción de dependencias propaga:

public class CustomMiddleware


{
private readonly RequestDelegate _next;

public CustomMiddleware(RequestDelegate next)


{
_next = next;
}

// IMyScopedService is injected into Invoke


public async Task Invoke(HttpContext httpContext, IMyScopedService svc)
{
svc.MyProperty = 1000;
await _next(httpContext);
}
}

Recursos adicionales
Migración de módulos y controladores HTTP a middleware de ASP.NET Core
Inicio de la aplicación en ASP.NET Core
Características de solicitud de ASP.NET Core
Activación de middleware basada en Factory en ASP.NET Core
Activación de middleware con un contenedor de terceros en ASP.NET Core
Middleware de ASP.NET Core
28/08/2018 • 19 minutes to read • Edit Online

Por Rick Anderson y Steve Smith


El software intermedio es un software que se ensambla en 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 se conocen como software intermedio o componentes de software intermedio.
Cada componente de software intermedio de la canalización de solicitudes es responsable de invocar
el siguiente componente de la canalización o de cortocircuitar la canalización, en caso de ser necesario.
En Migración de módulos y controladores HTTP a middleware de ASP.NET Core 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 software intermedio.

Creación de una canalización de software intermedio con


IApplicationBuilder
La canalización de solicitudes de ASP.NET Core consiste en una secuencia de delegados de solicitud a
los que se llama de uno en uno. En el siguiente diagrama se muestra este concepto. El subproceso 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 software intermedio 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 se llaman al principio de la canalización para que puedan capturar las excepciones que se
produzcan 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.

public class Startup


{
public void Configure(IApplicationBuilder app)
{
app.Run(async context =>
{
await context.Response.WriteAsync("Hello, World!");
});
}
}

El primer delegado de Run finaliza la canalización.


Encadene varios delegados de solicitudes con Use. El parámetro next representa el siguiente
delegado de la canalización. Si no llama al siguiente parámetro, puede cortocircuitar la canalización.
Normalmente, puede realizar acciones antes y después del siguiente delegado, tal como se muestra en
el ejemplo siguiente:

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. Si se modifica HttpResponse
después de haber iniciado la respuesta, se producirá una excepción. Por ejemplo, se producirá una excepción al
realizar cambios tales como el establecimiento de encabezados o el código. 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.
HasStarted es una sugerencia útil para indicar si se han enviado los encabezados o se han realizado escrituras
en el cuerpo.
Orden
El orden en el que se agregan los componentes de software intermedio en el método
Startup.Configure define el orden en el que se invocarán los componentes de software intermedio en
las solicitudes y el orden inverso de la respuesta. Por motivos de seguridad, rendimiento y
funcionalidad, el orden es básico.
El método Configure siguiente agrega los siguientes componentes de software intermedio:
1. Control de errores y excepciones
2. Servidor de archivos estáticos
3. Autenticación
4. MVC

public void Configure(IApplicationBuilder app)


{
if (env.IsDevelopment())
{
// When the app runs in the Development environment:
// Use the Developer Exception Page to report app runtime errors.
// Use the Database Error Page to report database runtime errors.
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
// When the app doesn't run in the Development environment:
// Enable the Exception Handler Middleware to catch exceptions
// thrown in the following middlewares.
// Use the HTTP Strict Transport Security Protocol (HSTS)
// Middleware.
app.UseExceptionHandler("/Error");
app.UseHsts();
}

// Use HTTPS Redirection Middleware to redirect HTTP requests to HTTPS.


app.UseHttpsRedirection();

// Return static files and end the pipeline.


app.UseStaticFiles();

// Use Cookie Policy Middleware to conform to EU General Data


// Protection Regulation (GDPR) regulations.
app.UseCookiePolicy();

// Authenticate before the user accesses secure resources.


app.UseAuthentication();

// Add MVC to the request pipeline.


app.UseMvc();
}
public void Configure(IApplicationBuilder app)
{
// Enable the Exception Handler Middleware to catch exceptions
// thrown in the following middlewares.
app.UseExceptionHandler("/Home/Error");

// Return static files and end the pipeline.


app.UseStaticFiles();

// Authenticate before you access secure resources.


app.UseIdentity();

// Add MVC to the request pipeline.


app.UseMvcWithDefaultRoute();
}

En el código de ejemplo anterior, cada método de extensión de software intermedio se expone en


IApplicationBuilder a través del espacio de nombres de Microsoft.AspNetCore.Builder.
UseExceptionHandler es el primer componente de software intermedio que se agrega a la
canalización. Por lo tanto, el software intermedio del controlador de excepciones detectará las
excepciones que se produzcan en las llamadas posteriores.
El software intermedio 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 software
intermedio no proporciona comprobaciones de autorización. Los archivos que proporciona, incluidos
los de wwwroot, están disponibles de forma pública. Para obtener más información sobre cómo
proteger este tipo de archivos, vea Archivos estáticos en ASP.NET Core.
Si el software intermedio de archivos estáticos no controla la solicitud, se pasará al software
intermedio de autenticación (UseAuthentication), que realizará la autenticación. Este software
intermedio no cortocircuita las solicitudes sin autenticación. Aunque autentique solicitudes, la
autorización (y también el rechazo) se producirán después de que MVC seleccione una página de
Razor o un control y una acción de MVC concretos.
Si el software intermedio de archivos estáticos no controla la solicitud, se pasará al software
intermedio de identidad (UseIdentity), que realizará la autenticación. Este middleware no cortocircuita
las solicitudes sin autenticación. Aunque autentique solicitudes, la autorización (y también el rechazo)
se producirán después de que MVC seleccione un control y una acción concretos.
En el ejemplo siguiente se muestra un orden de software intermedio en el que el software intermedio
de archivos estáticos controla las solicitudes de archivos estáticos antes del software intermedio de
compresión de respuestas. Los archivos estáticos no se comprimen en este orden de software
intermedio. Las respuestas de MVC de UseMvcWithDefaultRoute se pueden comprimir.

public void Configure(IApplicationBuilder app)


{
// Static files not compressed by Static Files Middleware.
app.UseStaticFiles();
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 Map , los segmentos de ruta que coincidan se eliminan de 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" processing
});
level1App.Map("/level2b", level2BApp => {
// "/level1/level2b" processing
});
});

Map puede hacer coincidir varios segmentos a la vez:


public class Startup
{
private static void HandleMultiSeg(IApplicationBuilder app)
{
app.Run(async context =>
{
await context.Response.WriteAsync("Map multiple segments.");
});
}

public void Configure(IApplicationBuilder app)


{
app.Map("/map1/seg1", HandleMultiSeg);

app.Run(async context =>


{
await context.Response.WriteAsync("Hello from non-Map delegate.");
});
}
}

Middleware integrado
ASP.NET Core incluye los componentes de software intermedio siguientes. En la columna Orden se
proporcionan notas sobre la ubicación del software intermedio en la solicitud de canalización, así como
las condiciones con las que podría finalizar la solicitud y evitar que otro software intermedio procese
una solicitud.

SOFTWARE INTERMEDIO DESCRIPCIÓN 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


recursos entre orígenes. usan CORS.

Diagnóstico Configura el diagnóstico. Antes de los componentes que


generan errores.

Encabezados reenviados Reenvía encabezados con proxy a Antes de los componentes que
la 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 Antes de que se envíen las


HTTP (HSTS) seguridad que agrega un respuestas y después de los
encabezado de respuesta especial componentes que modifican las
(ASP.NET Core 2.1 o posterior). solicitudes (por ejemplo,
encabezados reenviados,
reescritura de URL).
SOFTWARE INTERMEDIO DESCRIPCIÓN ORDEN

MVC Procesa la solicitudes con MVC o Si hay una solicitud que coincida
Razor Pages (ASP.NET Core 2.0 o con una ruta, será final.
versiones posteriores).

OWIN Puede interoperar con aplicaciones, Si el software intermedio de OWIN


servidores y software intermedio procesa completamente la solicitud,
basados en OWIN. será final.

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.

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 Si hay una solicitud que coincida
proporción de archivos estáticos y con un archivo, será final.
la 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 software intermedio


El middleware normalmente está encapsulado en una clase y se expone con un método de extensión.
Use el siguiente software intermedio 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}");
});

}
}

El código de ejemplo anterior se usa para mostrar la creación de un componente de software


intermedio. Para obtener más información sobre la compatibilidad con la localización integrada de
ASP.NET Core, vea Globalización y localización en 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 async 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


await _next(context);
}
}
}

El nombre del software intermedio del método 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 software intermedio 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 Startup.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. Si
necesita compartir servicios con software intermedio en una solicitud, vea la sección Dependencias
bajo solicitud.
Los componentes de software intermedio pueden resolver sus dependencias de una inserción de
dependencias (DI) mediante parámetros del constructor. UseMiddleware<T> también puede aceptar
parámetros adicionales directamente.
Dependencias bajo solicitud
Dado que el software intermedio se construye al inicio de la aplicación y no bajo solicitud, los servicios
de duración con ámbito que usan los constructores de software intermedio 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 la inserción de dependencias propaga:

public class CustomMiddleware


{
private readonly RequestDelegate _next;

public CustomMiddleware(RequestDelegate next)


{
_next = next;
}

// IMyScopedService is injected into Invoke


public async Task Invoke(HttpContext httpContext, IMyScopedService svc)
{
svc.MyProperty = 1000;
await _next(httpContext);
}
}

Recursos adicionales
Migración de módulos y controladores HTTP a middleware de ASP.NET Core
Inicio de la aplicación en ASP.NET Core
Características de solicitud de ASP.NET Core
Activación de middleware basada en Factory en ASP.NET Core
Activación de middleware con un contenedor de terceros en ASP.NET Core
Activación de middleware basada en Factory en
ASP.NET Core
31/08/2018 • 3 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 FactoryActivatedMiddleware : IMiddleware


{
private readonly AppDbContext _db;

public FactoryActivatedMiddleware(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 = "FactoryActivatedMiddleware",
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 UseFactoryActivatedMiddleware(


this IApplicationBuilder builder)
{
return builder.UseMiddleware<FactoryActivatedMiddleware>();
}
}

No se pueden pasar objetos al middleware activado por Factory con UseMiddleware :

public static IApplicationBuilder UseFactoryActivatedMiddleware(


this IApplicationBuilder builder, bool option)
{
// Passing 'option' as an argument throws a NotSupportedException at runtime.
return builder.UseMiddleware<FactoryActivatedMiddleware>(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<FactoryActivatedMiddleware>();

services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}

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();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseConventionalMiddleware();
app.UseFactoryActivatedMiddleware();

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.

Recursos adicionales
Middleware de ASP.NET Core
Activación de middleware con un contenedor de terceros en ASP.NET Core
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 Core 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
31/08/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)
{
CreateWebHostBuilder(args).Build().Run();
}

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>


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

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, IHostingEnvironment env)


{
var cachePeriod = env.IsDevelopment() ? "600" : "604800";
app.UseStaticFiles(new StaticFileOptions
{
OnPrepareResponse = ctx =>
{
// Requires the following import:
// using Microsoft.AspNetCore.Http;
ctx.Context.Response.Headers.Append("Cache-Control", $"public, max-age={cachePeriod}");
}
});
}

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) en el
entorno de desarrollo:

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
31/08/2018 • 42 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 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, 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 ASP.NET Core de bajo nivel. Para obtener información sobre el
enrutamiento de ASP.NET Core MVC, vea Enrutar a acciones de controlador de ASP.NET Core.

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, se 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 la sección
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 se suele basar en datos de la ruta de dirección URL, pero se puede ampliar para
tener en cuenta cualquier dato 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 que no sea NULL. 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
a next y se invoca el software intermedio siguiente de la canalización de solicitudes.
La entrada principal para RouteAsync es el RouteContext.HttpContext asociado a la solicitud actual.
RouteContext.Handler y RouteContext.RouteData son salidas que se establecen después de que una ruta
coincida.
Una coincidencia durante RouteAsync también establece 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 contiene información de estado importante sobre el
resultado.
RouteData.Values es un diccionario de los valores de ruta generados desde la ruta. Estos valores se suelen
determinar mediante la conversión en tokens de la dirección URL, y se pueden usar 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. 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 direcciones 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. Después, se llama
en secuencia al método GetVirtualPath de cada ruta hasta que se devuelva un valor VirtualPathData
distinto de NULL.
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.
TIP
Piense en VirtualPathContext.Values como un conjunto de invalidaciones para VirtualPathContext.AmbientValues. La
generación de direcciones 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 GetVirtualPathes VirtualPathData . VirtualPathData es un valor paralelo de RouteData .


VirtualPathData 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. Si quiere representar la dirección
URL generada en HTML, anteponga 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 hacen coincidir con la ruta de dirección URL
cuando se llama a RouteAsync. Route usa la misma plantilla de ruta para generar una dirección URL
cuando se llama a GetVirtualPath .
La mayoría de las aplicaciones crea rutas mediante una llamada a MapRoute o a uno de los métodos de
extensión similares definidos en IRouteBuilder. Todos estos métodos crean una instancia de Route y la
agregan a la colección de rutas.
MapRoute no toma un parámetro de controlador de ruta. MapRoute solo agrega las rutas que se controlan
mediante DefaultHandler. Dado que el controlador predeterminado es IRouter , es posible que decida no
controlar la solicitud. Por ejemplo, ASP.NET Core MVC se suele configurar como un controlador
predeterminado que solo controla las solicitudes que coinciden con un controlador y una acción
disponibles. Para obtener más información sobre el enrutamiento a MVC, vea Enrutar a acciones de
controlador de ASP.NET Core.
El código siguiente es un ejemplo de una llamada a MapRoute usada por una definición de ruta típica de
ASP.NET Core MVC:

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

Esta plantilla coincide con una ruta de dirección URL como /Products/Details/17 y extrae 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
en 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. Los parámetros opcionales no generan un valor de ruta si no existió el 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 coincide 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 se suelen usar
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?}");

TIP
La sintaxis insertada para definir las restricciones y los valores predeterminados puede ser conveniente para las rutas
simples. Pero hay algunas características, como los tokens de datos, que no son compatibles con esta sintaxis.

En el ejemplo siguiente se muestran algunos escenarios más:

routes.MapRoute(
name: "blog",
template: "Blog/{*article}",
defaults: new { controller = "Blog", action = "ReadArticle" });

Esta plantilla coincide con una ruta de dirección URL como /Blog/All-About-Routing/Introduction y extrae
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 coincide con una ruta de dirección URL como /en-US/Products/5 y extrae 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.

TIP
Para entender mejor la generación de direcciones 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 básica de estilo de ASP.NET Core MVC:

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

Con los valores de ruta { controller = Products, action = List } , esta ruta genera 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 genera la dirección URL / . Los
valores de ruta que se proporcionaron 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 realizan un recorrido de ida y vuelta con esta definición de ruta y
generan los mismos valores de ruta que se usaron para generar la dirección URL.
TIP
Las aplicaciones con ASP.NET Core MVC deben usar UrlHelper para generar direcciones URL en lugar de llamar
directamente al enrutamiento.

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


Haga referencia al metapaquete Microsoft.AspNetCore.App en el archivo de proyecto de la aplicación.
Haga referencia al metapaquete Microsoft.AspNetCore.All en el archivo de proyecto de la aplicación.
Haga referencia a Microsoft.AspNetCore.Routing en el archivo de proyecto de la aplicación.
Agregue enrutamiento al contenedor de servicios en Startup.ConfigureServices :

public void ConfigureServices(IServiceCollection services)


{
services.AddRouting();
}

public void ConfigureServices(IServiceCollection services)


{
services.AddRouting();
}

Las rutas se deben configurar en el método Startup.Configure . En la aplicación de ejemplo se usan estas
API:
RouteBuilder
Build
MapGet – solo coincide con solicitudes HTTP GET.
UseRouter
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");
// 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);

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");
// 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>


IDENTIFICADOR URI RESPUESTA

GET /hello/Joe Hi, Joe!

POST /hello/Joe <Pasar explícitamente, solo coincide con HTTP GET>

GET /hello/Joe/Smith <Pasar explícitamente, ninguna coincidencia>

Si va a configurar una única ruta, pase una instancia de IRouter para llamar a UseRouter. No tendrá que
usar 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 usa como controlador de ruta cuando la ruta coincida. Otros métodos de esta familia
permiten configurar una canalización de software intermedio para usarla como controlador de ruta. Si el
método Map no acepta un controlador, como MapRoute , usa 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 enlazan 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 es 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, considere la
plantilla files/{filename}.{ext?} . Cuando existen filename y ext , ambos valores se rellenan. Si solo
existe filename en la dirección URL, la ruta coincide porque el punto final . es opcional. Las direcciones
URL siguientes coinciden con esta ruta:
/files/myFile.txt
/files/myFile

Se 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} coincide con cualquier URI que
empiece por /blog y que vaya seguido de cualquier valor (que se asigna 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 igual ( = ). Por ejemplo,
{controller=Home} define 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 de interrogación ( ? )
al final del nombre del parámetro, como en id? ). La diferencia entre los valores opcionales y los
parámetros de ruta predeterminados 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 la dirección URL de
solicitud le 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 una restricción minlength con el argumento
10 . Para obtener más información sobre las restricciones de ruta y una lista de las restricciones
proporcionadas por el marco de trabajo, vea la sección Referencia de restricciones 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.
TIP
Habilite el registro para ver de qué forma las implementaciones de enrutamiento integradas, como Route , coinciden
con las solicitudes.

Nombres de enrutamientos reservados


Las siguientes palabras clave son nombres reservados y no se pueden usar como nombres de ruta o
parámetros:
action
area
controller
handler
page

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 - Solicitud incorrecta con el mensaje de error adecuado. Las
restricciones de ruta se usan 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)
RESTRICCIÓN EJEMPLO COINCIDENCIAS DE EJEMPLO NOTAS

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

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
RESTRICCIÓN EJEMPLO COINCIDENCIAS DE EJEMPLO NOTAS

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

Es posible aplicar varias restricciones delimitadas por dos puntos a un único parámetro. Por ejemplo, la
siguiente restricción permite limitar un parámetro a un valor entero de 1 o superior:

[Route("users/{id:int:min(1)}")]
public User GetUserById(int id) { }

WARNING
Las restricciones de ruta que comprueban la dirección URL y que se convierten en un tipo CLR (como int o
DateTime ) usan siempre la referencia cultural invariable. Estas restricciones 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 almacenan como
cadenas. Por ejemplo, la restricción float intenta 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
RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariantal constructor de
expresiones regulares. Vea RegexOptions para obtener 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. Para usar la
expresión regular ^\d{3}-\d{2}-\d{4}$ en el enrutamiento, es necesario escribir los caracteres \ de la
expresión 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 se usen 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 CON SECUENCIA DE ESCAPE

^\d{3}-\d{2}-\d{4}$ ^\\d{{3}}-\\d{{2}}-\\d{{4}}$
EXPRESIÓN CON SECUENCIA DE ESCAPE

^[a-z]{2}$ ^[[a-z]]{{2}}$

Las expresiones regulares que se usan en el enrutamiento suelen empezar con el carácter ^ (coincidencia
con la posición inicial de la cadena) y terminar 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 coincide con cualquier subcadena de la
cadena, algo que normalmente 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)$ es 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/>");
});

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 diccionario


proporciona los valores de ruta operation e id de la plantilla "Ruta de paquete de seguimiento",
package/{operation}/{id} . Para obtener más información, vea el código de ejemplo de la sección Uso de
software intermedio de enrutamiento o la aplicación de ejemplo.
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. Si en una aplicación ASP.NET
Core 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


VALORES DE AMBIENTE VALORES EXPLÍCITOS RESULTADO

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 de forma explícita, debe coincidir con el valor predeterminado:

routes.MapRoute("blog_route", "blog/{*slug}",
defaults: new { controller = "Blog", action = "ReadPost" });

La generación de vínculos solo genera 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
29/06/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);
}

NOTE
Al redirigir a HTTPS sin la necesidad de disponer de reglas de redirección adicionales, se recomienda usar el Middleware de
redirección de HTTPS. Para más información, vea el tema Aplicación de HTTPS.

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 o cree una excepción para confiar en el certificado en cuestión.
Solicitud original mediante AddRedirectToHttps(301, 5001) : http://localhost:5000/secure
Solicitud original mediante AddRedirectToHttpsPermanent : http://localhost:5000/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
31/08/2018 • 15 minutes to read • Edit Online

Por Rick Anderson


ASP.NET Core configura el comportamiento de las aplicaciones en función del entorno en tiempo de
ejecución mediante una variable 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 el valor
en IHostingEnvironment.EnvironmentName. Puede establecer ASPNETCORE_ENVIRONMENT en cualquier valor,
pero el marco de trabajo admite tres valores: Development, Staging y Production. Si no se establece
ASPNETCORE_ENVIRONMENT , el valor predeterminado es Production .

public void Configure(IApplicationBuilder app, IHostingEnvironment env)


{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}

if (env.IsProduction() || env.IsStaging() || env.IsEnvironment("Staging_2"))


{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseMvc();
}

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>

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_My_Environment": "1",
"ASPNETCORE_DETAILEDERRORS": "1",
"ASPNETCORE_ENVIRONMENT": "Staging"
}
},
"EnvironmentsSample": {
"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:

"EnvironmentsSample": {
"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 usa el primer perfil con "commandName": "Project" . El valor de
commandName especifica el servidor web que se va a iniciar. commandName puede ser uno de los siguientes:

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:\Websites\EnvironmentsSample> dotnet run


Using launch settings from C:\Websites\EnvironmentsSample\Properties\launchSettings.json...
Hosting environment: Staging
Content root path: C:\Websites\EnvironmentsSample
Now listening on: http://localhost:54340
Application started. Press Ctrl+C to shut down.

La pestaña Depurar de las propiedades de proyecto 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 pueda detectar 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.

Cuando se usa Visual Studio Code, las variables de entorno se pueden establecer en el archivo
.vscode/launch.json. En el siguiente ejemplo se establece el entorno en Development :
{
"version": "0.2.0",
"configurations": [
{
"name": ".NET Core Launch (web)",

... additional VS Code configuration settings ...

"env": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
]
}

Un archivo .vscode/launch.json del proyecto no se lee al iniciar la aplicación con dotnet run en la misma
manera que Properties/launchSettings.json. Cuando se inicia una aplicación en desarrollo que no tiene un
archivo launchSettings.json, establezca el valor del entorno con una variable de entorno o con un argumento
de línea de comandos para el comando dotnet run .
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 es 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 App Service
Para establecer el entorno en Azure App Service, realice los pasos siguientes:
1. Seleccione la aplicación desde la hoja App Services.
2. En el grupo CONFIGURACIÓN, seleccione la hoja Configuración de la aplicación.
3. En el área Configuración de la aplicación, haga clic en Agregar nueva configuración.
4. Para Escriba un nombre, proporcione ASPNETCORE_ENVIRONMENT . Para Escriba un valor, proporcione el
entorno (por ejemplo, Staging ).
5. Active la casilla Configuración de ranuras si quiere que la configuración del entorno permanezca con la
ranura actual cuando se intercambien las ranuras de implementación. Para obtener más información, vea
Configuración de entornos de ensayo en Azure App Service.
6. Haga clic en Guardar en la parte superior de la hoja.
Azure App Service reinicia automáticamente la aplicación después de que se agregue, cambie o elimine una
configuración de aplicación (variable de entorno) en Azure Portal.
Windows
Para establecer ASPNETCORE_ENVIRONMENT en la sesión actual, cuando la aplicación se ha iniciado con dotnet run,
use los comandos siguientes:
Símbolo del sistema

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, use cualquiera de los métodos siguientes:
Abra Panel de control > Sistema > Configuración avanzada del sistema y agregue o edite el
valor ASPNETCORE_ENVIRONMENT :

Abra un símbolo del sistema con permisos de administrador y use el comando setx o abra un
símbolo del sistema administrativo de PowerShell y use [Environment]::SetEnvironmentVariable :
Símbolo del sistema

setx ASPNETCORE_ENVIRONMENT Development /M

El modificador /M indica que hay que establecer la variable de entorno en el nivel de sistema. Si no se
usa el modificador /M , la variable de entorno se establece para la cuenta de usuario.
PowerShell
[Environment]::SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development", "Machine")

El valor de opción Machine indica que hay que establecer la variable de entorno en el nivel de sistema.
Si el valor de opción se cambia a User , la variable de entorno se establece para la cuenta de usuario.

Cuando la variable de entorno ASPNETCORE_ENVIRONMENT se establece de forma global, se aplica a dotnet run
en cualquier ventana de comandos abierta después del establecimiento del valor.
web.config
Para establecer la variable de entorno ASPNETCORE_ENVIRONMENT con web.config, vea la sección Establecimiento
de variables de entorno de Referencia de configuración del módulo ASP.NET Core. Cuando la variable de
entorno ASPNETCORE_ENVIRONMENT se establece con web.config, su valor reemplaza a un valor en el nivel de
sistema.
Por grupo de aplicaciones de IIS
Para establecer la variable de entorno ASPNETCORE_ENVIRONMENT para una aplicación que se ejecuta en un grupo
de aplicaciones aislado (se admite en IIS 10.0 o posterior), vea la sección AppCmd.exe del tema Environment
Variables <environmentVariables> (Variables de entorno). Cuando la variable de entorno
ASPNETCORE_ENVIRONMENT se establece para un grupo de aplicaciones, su valor reemplaza a un valor en el nivel
de sistema.

IMPORTANT
Cuando hospede una aplicación en IIS y agregue o cambie la variable de entorno ASPNETCORE_ENVIRONMENT , use
cualquiera de los siguientes métodos para que las aplicaciones tomen el nuevo valor:
Reinicie el grupo de aplicaciones de una aplicación.
Ejecute net stop was /y seguido de net start w3svc en un símbolo del sistema.
Reinicie el servidor.

macOS
Para establecer el entorno actual para macOS, puede hacerlo en línea al ejecutar la aplicación:

ASPNETCORE_ENVIRONMENT=Development dotnet run

De forma alternativa, defina el entorno con export 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. Edite el archivo
con cualquier editor de texto. Agregue la siguiente instrucción:

export ASPNETCORE_ENVIRONMENT=Development

Linux
Para distribuciones de Linux, use el comando export en un símbolo del sistema 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 cargar la configuración por entorno, se recomienda lo siguiente:
Archivos appSettings (*appsettings.<>.json). Vea Configuración: proveedor de configuración de archivo.
Variables de entorno (establecidas en todos los sistemas donde se hospede la aplicación). Vea
Configuración: proveedor de configuración de archivo y Almacenamiento seguro de secretos de aplicación
en desarrollo: variables de entorno.
Administrador de secretos (solo en el entorno de desarrollo). Vea Almacenamiento seguro de secretos de
aplicación en el desarrollo en ASP.NET Core.

Métodos y clase Startup basados en entorno


Convenciones de la clase Startup
Cuando se inicia una aplicación ASP.NET Core, la clase Startup arranca la aplicación. La aplicación puede
definir clases Startup independientes para distintos entornos (por ejemplo, StartupDevelopment ) y la clase
Startup correspondiente se selecciona en tiempo de ejecución. La clase cuyo sufijo de nombre coincide con
el entorno actual se establece como prioritaria. Si no se encuentra una clase Startup{EnvironmentName}
coincidente, se usa la clase Startup .
Para implementar clases Startup basadas en entornos, cree una clase Startup{EnvironmentName} para cada
entorno en uso y una clase Startup de reserva:

// Startup class to use in the Development environment


public class StartupDevelopment
{
public void ConfigureServices(IServiceCollection services)
{
...
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)


{
...
}
}

// Startup class to use in the Production environment


public class StartupProduction
{
public void ConfigureServices(IServiceCollection services)
{
...
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)


{
...
}
}

// Fallback Startup class


// Selected if the environment doesn't match a Startup{EnvironmentName} class
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
...
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)


{
...
}
}
Use la sobrecarga UseStartup(IWebHostBuilder, String) que acepta un nombre de ensamblado:

public static void Main(string[] args)


{
CreateWebHostBuilder(args).Build().Run();
}

public static IWebHostBuilder CreateWebHostBuilder(string[] args)


{
var assemblyName = typeof(Startup).GetTypeInfo().Assembly.FullName;

return WebHost.CreateDefaultBuilder(args)
.UseStartup(assemblyName);
}

public static void Main(string[] args)


{
CreateWebHost(args).Run();
}

public static IWebHost CreateWebHost(string[] args)


{
var assemblyName = typeof(Startup).GetTypeInfo().Assembly.FullName;

return WebHost.CreateDefaultBuilder(args)
.UseStartup(assemblyName)
.Build();
}

public class Program


{
public static void Main(string[] args)
{
var assemblyName = typeof(Startup).GetTypeInfo().Assembly.FullName;

var host = new WebHostBuilder()


.UseStartup(assemblyName)
.Build();

host.Run();
}
}

Convenciones del método Startup


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)


{
StartupConfigureServices(services);
}

public void ConfigureStagingServices(IServiceCollection services)


{
StartupConfigureServices(services);
}

private void StartupConfigureServices(IServiceCollection services)


{
services.Configure<CookiePolicyOptions>(options =>
{
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();
}

if (env.IsProduction() || env.IsStaging() || env.IsEnvironment("Staging_2"))


{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseMvc();
}

public void ConfigureStaging(IApplicationBuilder app, IHostingEnvironment env)


{
if (!env.IsStaging())
{
throw new Exception("Not staging.");
}

app.UseExceptionHandler("/Error");
app.UseHsts();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseMvc();
}
}
Recursos adicionales
Inicio de la aplicación en ASP.NET Core
Configuración en ASP.NET Core
IHostingEnvironment.EnvironmentName
Configuración en ASP.NET Core
07/09/2018 • 57 minutes to read • Edit Online

Por Luke Latham


La configuración de la aplicación en ASP.NET Core se basa en pares clave-valor establecidos por proveedores de
configuración. Los proveedores de configuración leen los datos de configuración en los pares clave-valor de
distintos orígenes de configuración:
Azure Key Vault
Argumentos de la línea de comandos
Proveedores personalizados (instalados o creados)
Archivos de directorio
Variables de entorno
Objetos de .NET en memoria
Archivos de configuración
Azure Key Vault
Argumentos de la línea de comandos
Proveedores personalizados (instalados o creados)
Variables de entorno
Objetos de .NET en memoria
Archivos de configuración
Argumentos de la línea de comandos
Proveedores personalizados (instalados o creados)
Variables de entorno
Objetos de .NET en memoria
Archivos de configuración
El patrón de opciones es una extensión de los conceptos de configuración que se describen en este tema. Las
opciones usan clases para representar grupos de configuraciones relacionadas. Para más información sobre cómo
usar el patrón de opciones, consulte Patrón de opciones en ASP.NET Core.
Vea o descargue el código de ejemplo (cómo descargarlo)
Los ejemplos proporcionados en este tema dependen de lo siguiente:
Establecer la ruta base de la aplicación con SetBasePath. SetBasePath está disponible para una aplicación
mediante una referencia al paquete Microsoft.Extensions.Configuration.FileExtensions.
Resolver las secciones de los archivos de configuración con GetSection. GetSection está disponible para una
aplicación mediante una referencia al paquete Microsoft.Extensions.Configuration.
Enlazar la configuración a clases .NET con Bind y Get<T>. Bind y Get<T> están disponibles para una aplicación
mediante una referencia al paquete Microsoft.Extensions.Configuration.Binder. Get<T> está disponible en
ASP.NET Core 1.1 o versiones posteriores.
Estos tres paquetes están incluidos en el metapaquete Microsoft.AspNetCore.App.
Estos tres paquetes están incluidos en el metapaquete Microsoft.AspNetCore.All.
Configuración de host frente a configuración de aplicación
Antes de configurar e iniciar la aplicación, se configura e inicia un host. El host es responsable de la administración
del inicio y la duración de la aplicación. Tanto la aplicación como el host se configuran mediante los proveedores de
configuración que se describen en este tema. Los pares clave-valor de la configuración de host se vuelven parte de
la configuración global de la aplicación. Para más información sobre cómo se usan los proveedores de
configuración cuando se compila el host y cómo afectan los orígenes de configuración a la configuración del host,
consulte Hospedaje en ASP.NET Core.

Seguridad
Adopte estos procedimientos recomendados:
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 varios entornos y cómo administrar el almacenamiento seguro de
secretos de aplicación en el desarrollo con el Administrador de secretos (incluye consejos sobre el uso de variables
de entorno para almacenar información confidencial). El Administrador de secretos usa el proveedor de
configuración de archivo para almacenar secretos de usuario en un archivo JSON del sistema local. El proveedor
de configuración de archivo se describe más adelante en este tema.
Azure Key Vault es una opción para el almacenamiento seguro de los secretos de aplicación. Para obtener más
información, vea Proveedor de configuración de Azure Key Vault en ASP.NET Core.

Datos de configuración jerárquica


La API de configuración es capaz de mantener los datos de configuración jerárquica al disminuir los datos
jerárquicos mediante el uso de un delimitador en las claves de configuración.
En el siguiente archivo JSON, hay cuatro claves en una estructura jerárquica de dos secciones:

{
"section0": {
"key0": "value",
"key1": "value"
},
"section1": {
"key0": "value",
"key1": "value"
}
}

Cuando el archivo se lee en la configuración, se crean claves únicas para mantener la estructura de datos
jerárquicos original del origen de configuración. Las secciones y claves se disminuyen con el uso de dos puntos ( :
) para mantener la estructura original:
section0:key0
section0:key1
section1:key0
section1:key1
Los métodos GetSection y GetChildren están disponibles para aislar las secciones y los elementos secundarios de
una sección en los datos de configuración. Estos métodos se describen más adelante en la sección GetSection,
GetChildren y Exists.

Convenciones
Al iniciar la aplicación, los orígenes de configuración se leen en el orden en que se especifican sus proveedores de
configuración.
Los proveedores de configuración de archivo tienen la capacidad de recargar la configuración cuando se modifica
un archivo de configuración subyacente después del inicio de la aplicación. El proveedor de configuración de
archivo se describe más adelante en este tema.
IConfiguration está disponible en el contenedor Inserción de dependencias (DI) de la aplicación. Los proveedores
de configuración no pueden usar la inserción de dependencias, porque no está disponible cuando el host los
configura.
Las claves de configuración adoptan las convenciones siguientes:
Las claves no distinguen mayúsculas de minúsculas. Por ejemplo, ConnectionString y connectionstring se
consideran claves equivalente.
Si los mismos o distintos proveedores de configuración establecen un valor para la misma clave, el último valor
establecido en la clave es el valor que se usa.
Claves jerárquicas
Dentro de la API de configuración, un separador de dos puntos ( : ) funciona en todas las plataformas.
En las variables de entorno, puede que un separador de dos puntos no funcione en todas las plataformas.
Un guion bajo doble ( __ ) es compatible con todas las plataformas y se convierte en un signo de dos
puntos.
En Azure Key Vault, las claves jerárquicas usan -- (dos guiones) como separador. Debe proporcionar
código para reemplazar los guiones por dos puntos cuando los secretos se cargan en la configuración de
la aplicación.
ConfigurationBinder admite enlazar matrices a objetos con los índices de matriz en las claves de configuración.
El enlace de matriz se describe en la sección Enlace de una matriz a una clase.
Los valores de configuración adoptan las convenciones siguientes:
Los valores son cadenas.
Los valores NULL no se pueden almacenar en la configuración ni enlazar a los objetos.

Proveedores
La siguiente tabla muestra los proveedores de configuración disponibles para las aplicaciones de ASP.NET Core.

PROVEEDOR PROPORCIONA LA CONFIGURACIÓN DE…

Proveedor de configuración de Azure Key Vault (temas de Azure Key Vault


Seguridad)

Proveedor de configuración de línea de comandos Parámetros de la línea de comandos

Proveedor de configuración personalizada Origen personalizado

Proveedor de configuración de variables de entorno Variables de entorno

Proveedor de configuración de archivo Archivos (INI, JSON, XML)


PROVEEDOR PROPORCIONA LA CONFIGURACIÓN DE…

Proveedor de configuración de clave por archivo Archivos de directorio

Proveedor de configuración de memoria Colecciones en memoria

Secretos de usuario (Administrador de secretos) (temas de Archivo en el directorio del perfil de usuario
Seguridad)

PROVEEDOR PROPORCIONA LA CONFIGURACIÓN DE…

Proveedor de configuración de Azure Key Vault (temas de Azure Key Vault


Seguridad)

Proveedor de configuración de línea de comandos Parámetros de la línea de comandos

Proveedor de configuración personalizada Origen personalizado

Proveedor de configuración de variables de entorno Variables de entorno

Proveedor de configuración de archivo Archivos (INI, JSON, XML)

Proveedor de configuración de memoria Colecciones en memoria

Secretos de usuario (Administrador de secretos) (temas de Archivo en el directorio del perfil de usuario
Seguridad)

PROVEEDOR PROPORCIONA LA CONFIGURACIÓN DE…

Proveedor de configuración de línea de comandos Parámetros de la línea de comandos

Proveedor de configuración personalizada Origen personalizado

Proveedor de configuración de variables de entorno Variables de entorno

Proveedor de configuración de archivo Archivos (INI, JSON, XML)

Proveedor de configuración de memoria Colecciones en memoria

Secretos de usuario (Administrador de secretos) (temas de Archivo en el directorio del perfil de usuario
Seguridad)

Los orígenes de configuración se leen en el orden en que se especifican sus proveedores de configuración en el
inicio. En este tema, los proveedores de configuración se describen en orden alfabético y no en el orden en que el
código podría organizarlos. Ordene los proveedores de configuración en el código para cumplir con sus
prioridades relacionadas con los orígenes de configuración subyacentes.
Esta es una secuencia típica de proveedores de configuración:
1. Archivos (appsettings.json, appsettings.<Environment>.json, donde <Environment> es el entorno de hospedaje
actual de la aplicación)
2. Secretos de usuario (Administrador de secretos) (solo en el entorno de desarrollo)
3. Variables de entorno
4. Argumentos de la línea de comandos
Una práctica común es colocar el proveedor de configuración de línea de comandos al final de en una serie de
proveedores para permitir que los argumentos de la línea de comandos invaliden la configuración que
establecieron los demás proveedores.
Esta secuencia de los proveedores se aplica al inicializar un nuevo WebHostBuilder con CreateDefaultBuilder. Para
más información, consulte Host web: Configuración de un host.
Llame a ConfigureAppConfiguration cuando cree el host web para especificar los proveedores de configuración de
la aplicación:

public class Program


{
public static Dictionary<string, string> arrayDict = new Dictionary<string, string>
{
{"array:entries:0", "value0"},
{"array:entries:1", "value1"},
{"array:entries:2", "value2"},
{"array:entries:4", "value4"},
{"array:entries:5", "value5"}
};

public static void Main(string[] args)


{
CreateWebHostBuilder(args).Build().Run();
}

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>


WebHost.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((hostingContext, config) =>
{
config.SetBasePath(Directory.GetCurrentDirectory());
config.AddInMemoryCollection(arrayDict);
config.AddJsonFile("json_array.json", optional: false, reloadOnChange: false);
config.AddJsonFile("starship.json", optional: false, reloadOnChange: false);
config.AddXmlFile("tvshow.xml", optional: false, reloadOnChange: false);
config.AddEFConfiguration(options => options.UseInMemoryDatabase("InMemoryDb"));
config.AddCommandLine(args);
})
.UseStartup<Startup>();
}

ConfigureAppConfiguration está disponible en ASP.NET Core 2.1 o versiones posteriores.


Esta secuencia de proveedores se puede crear para la aplicación (no para el host) con ConfigurationBuilder y una
llamada a su método Build en Startup :
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true,
reloadOnChange: true);

var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName));

if (appAssembly != null)
{
builder.AddUserSecrets(appAssembly, optional: true);
}

builder.AddEnvironmentVariables();

Configuration = builder.Build();
}

public IConfiguration Configuration { get; }

public void ConfigureServices(IServiceCollection services)


{
services.AddSingleton<IConfiguration>(Configuration);
}

En el ejemplo anterior, IHostingEnvironment proporciona el nombre del entorno ( env.EnvironmentName ) y el


nombre del ensamblado de la aplicación ( env.ApplicationName ). Para obtener más información, vea Usar varios
entornos en ASP.NET Core.

Proveedor de configuración de línea de comandos


CommandLineConfigurationProvider carga la configuración de pares clave-valor de argumento de la línea de
comandos en tiempo de ejecución.
Para activar la configuración de línea de comandos, llame al método de extensión AddCommandLine en una
instancia de ConfigurationBuilder.
AddCommandLine se llama automáticamente cuando se inicializa un nuevo WebHostBuilder con
CreateDefaultBuilder. Para más información, consulte Host web: Configuración de un host.
CreateDefaultBuilder también carga:
Configuración opcional de appsettings.json y appsettings.< Environment>.json.
Secretos de usuario (Administrador de secretos) (en el entorno de desarrollo).
Variables de entorno.
CreateDefaultBuilderagrega el proveedor de configuración de línea de comandos al final. Los argumentos de la
línea de comandos que se pasan en tiempo de ejecución invalidan la configuración establecida por los otros
proveedores.
CreateDefaultBuilder actúa cuando se construye el host. Por tanto, la configuración de línea de comandos activada
por CreateDefaultBuilder puede afectar la manera en que se configura el host.
Al crear el host manualmente y no mediante una llamada a CreateDefaultBuilder , llame al método de extensión
AddCommandLine en una instancia de ConfigurationBuilder:

Para activar la configuración de línea de comandos, llame al método de extensión AddCommandLine en una
instancia de ConfigurationBuilder.
Llame en último lugar al proveedor para permitir que los argumentos de la línea de comandos que se pasan en
tiempo de ejecución invaliden la configuración establecida por los otros proveedores de configuración.
Aplique la configuración a WebHostBuilder con el método UseConfiguration:

var config = new ConfigurationBuilder()


.AddCommandLine(args)
.Build();

var host = new WebHostBuilder()


.UseConfiguration(config)
.UseKestrel()
.UseStartup<Startup>();

Ejemplo
La aplicación de ejemplo 2.x aprovecha el método de conveniencia estático CreateDefaultBuilder para crear el host,
que incluye una llamada a AddCommandLine.
La aplicación de ejemplo 1.x llama a AddCommandLine en ConfigurationBuilder.
1. Abra un símbolo del sistema en el directorio del proyecto.
2. Proporcione un argumento de la línea de comandos para el comando dotnet run ,
dotnet run CommandLineKey=CommandLineValue .
3. Una vez que se ejecuta la aplicación, abra un explorador a la aplicación en http://localhost:5000 .
4. Observe que la salida contiene el par clave-valor para el argumento de línea de comandos de configuración
proporcionado a dotnet run .
Argumentos
El valor debe seguir a un signo igual ( = ) o la clave debe tener un prefijo ( -- o / ) cuando el valor siga a un
espacio. El valor puede ser NULL si se usa un signo igual (por ejemplo, CommandLineKey= ).

PREFIJO DE LA CLAVE EJEMPLO

Sin prefijo CommandLineKey1=value1

Dos guiones ( -- ) --CommandLineKey2=value2 , --CommandLineKey2 value2

Barra diagonal ( / ) /CommandLineKey3=value3 , /CommandLineKey3 value3

Dentro del mismo comando, no mezcle pares clave-valor de argumento de la línea de comandos que usan un signo
igual con pares de clave-valor que usan un espacio.
Comandos de ejemplo:

dotnet run CommandLineKey1=value --CommandLineKey2=value /CommandLineKey2=value


dotnet run --CommandLineKey1 value /CommandLineKey2 value
dotnet run CommandLineKey1= CommandLineKey2=value

Asignaciones de modificador
Las asignaciones de modificador admiten la lógica de sustitución de nombres de clave. Cuando crea manualmente
la configuración con ConfigurationBuilder, puede proporcionar un diccionario de reemplazos de modificador al
método AddCommandLine.
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 el par
clave-valor en la configuración de la aplicació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.

public class Program


{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}

public static IWebHostBuilder CreateWebHostBuilder(string[] args)


{
var switchMappings = new Dictionary<string, string>
{
{ "-CLKey1", "CommandLineKey1" },
{ "-CLKey2", "CommandLineKey2" }
};

var config = new ConfigurationBuilder()


.AddCommandLine(args, switchMappings)
.Build();

return WebHost.CreateDefaultBuilder()
.UseConfiguration(config)
.UseStartup<Startup>();
}
}

Como se muestra en el ejemplo anterior, la llamada a CreateDefaultBuilder no debería pasar argumentos cuando
se usan las asignaciones de modificador. La llamada de AddCommandLine del método CreateDefaultBuilder no
incluye modificadores asignados y no hay forma de pasar el diccionario de asignación de modificador a
CreateDefaultBuilder . Si los argumentos incluyen un conmutador asignado y se pasan a CreateDefaultBuilder , el
proveedor de AddCommandLine no se puede inicializar con una FormatException. La solución no es pasar los
argumentos de CreateDefaultBuilder , sino que permitir que el método AddCommandLine del método
ConfigurationBuilder procese tanto los argumentos como el diccionario de asignación de modificador.
public static void Main(string[] args)
{
var switchMappings = new Dictionary<string, string>
{
{ "-CLKey1", "CommandLineKey1" },
{ "-CLKey2", "CommandLineKey2" }
};

var config = new ConfigurationBuilder()


.AddCommandLine(args, switchMappings)
.Build();

var host = new WebHostBuilder()


.UseConfiguration(config)
.UseKestrel()
.UseStartup<Startup>()
.Start();

using (host)
{
Console.ReadLine();
}
}

Después de crear el diccionario de asignaciones de modificador, contiene los datos que se muestran en la tabla
siguiente.

KEY VALOR

-CLKey1 CommandLineKey1

-CLKey2 CommandLineKey2

Si las claves de asignación de conmutador se usan al iniciar la aplicación, la configuración recibe el valor de
configuración en la clave que el diccionario suministra:

dotnet run -CLKey1=value1 -CLKey2=value2

Una vez que se ejecuta el comando anterior, la configuración contiene los valores que se muestran en la tabla
siguiente.

KEY VALOR

CommandLineKey1 value1

CommandLineKey2 value2

Proveedor de configuración de variables de entorno


EnvironmentVariablesConfigurationProvider carga la configuración de pares clave-valor de variables de entorno en
tiempo de ejecución.
Para activar la configuración de variables de entorno, llame al método de extensión AddEnvironmentVariables en
una instancia de ConfigurationBuilder.
Al trabajar con claves jerárquicas en variables de entorno, es posible que un separador de dos puntos ( : ) no
funcione en todas las plataformas. Un guion bajo doble ( __ ) es compatible con todas las plataformas y lo
reemplaza un separador de dos puntos.
Azure App Service permite establecer las variables de entorno en Azure Portal que pueden invalidar la
configuración de la aplicación mediante el proveedor de configuración de variables de entorno. Para más
información, consulte el artículo sobre Azure Apps: Invalidación de la configuración de la aplicación con Azure
Portal.
AddEnvironmentVariables se llama automáticamente cuando se inicializa un nuevo WebHostBuilder con
CreateDefaultBuilder. Para más información, consulte Host web: Configuración de un host.
CreateDefaultBuilder también carga:
Configuración opcional de appsettings.json y appsettings.< Environment>.json.
Secretos de usuario (Administrador de secretos) (en el entorno de desarrollo).
Argumentos de la línea de comandos.
El proveedor de configuración de variables de entorno se llama una vez establecida la configuración desde los
secretos de usuario y los archivos appsettings. Llamar al proveedor en esta posición permite que la lectura de las
variables de entorno en tiempo de ejecución invaliden la configuración establecida por los secretos de usuario y los
archivos appsettings.
Puede llamar directamente al método de extensión AddEnvironmentVariables en una instancia de
ConfigurationBuilder:
Aplique la configuración a WebHostBuilder con el método UseConfiguration :

var config = new ConfigurationBuilder()


.AddEnvironmentVariables()
.Build();

var host = new WebHostBuilder()


.UseConfiguration(config)
.UseKestrel()
.UseStartup<Startup>();

Ejemplo
La aplicación de ejemplo 2.x aprovecha el método de conveniencia estático CreateDefaultBuilder para crear el host,
que incluye una llamada a AddEnvironmentVariables .
La aplicación de ejemplo 1.x llama a AddEnvironmentVariables en ConfigurationBuilder .
1. Ejecute la aplicación de ejemplo. Abra un explorador a la aplicación en http://localhost:5000 .
2. Observe que el resultado contiene el par clave y valor para la variable de entorno ENVIRONMENT . El valor refleja el
entorno en el que se ejecuta la aplicación, habitualmente Development cuando se ejecuta de manera local.
Para que la lista de variables de entorno que muestra la aplicación sea breve, la aplicación filtra las variables de
entorno que comienzan con lo siguiente:
ASPNETCORE_
urls
Registro
ENVIRONMENT
contentRoot
AllowedHosts
applicationName
CommandLine
Si quiere exponer todas las variables de entorno disponibles para la aplicación, cambie FilteredConfiguration en
Pages/Index.cshtml.cs a lo siguiente:
Si quiere exponer todas las variables de entorno disponibles para la aplicación, cambie FilteredConfiguration en
Controllers/HomeController.cs a lo siguiente:

FilteredConfiguration = _config.AsEnumerable();

Prefijos
Las variables de entorno que se cargan en la configuración de la aplicación se filtran cuando se proporciona un
prefijo para el método AddEnvironmentVariables . Por ejemplo, para filtrar las variables de entorno en el prefijo
CUSTOM_ , suministre el prefijo para el proveedor de configuración:

var config = new ConfigurationBuilder()


.AddEnvironmentVariables("CUSTOM_")
.Build();

El prefijo se quita cuando se crean los pares clave-valor de configuración.


El método de conveniencia estático CreateDefaultBuilder crea un WebHostBuilder para establecer el host de la
aplicación. Cuando se crea WebHostBuilder , encuentra su configuración de host en variables de entorno con el
prefijo ASPNETCORE_ .
Prefijos de cadena de conexión
La API de configuración tiene reglas de procesamiento especial para cuatro variables de entorno de cadena de
conexión relacionadas con la configuración de las cadenas de conexión de Azure para el entorno de la aplicación.
Las variables de entorno con los prefijos que se muestran en la tabla se cargan en la aplicación si no se proporciona
ningún prefijo a AddEnvironmentVariables .

PREFIJO DE CADENA DE CONEXIÓN PROVEEDOR

CUSTOMCONNSTR_ Proveedor personalizado

MYSQLCONNSTR_ MySQL

SQLAZURECONNSTR_ Azure SQL Database

SQLCONNSTR_ SQL Server

Cuando una variable de entorno se detecta y carga en la configuración con cualquiera de los cuatro prefijos que se
muestran en la tabla:
La clave de configuración se crea al quitar el prefijo de la variable de entorno y al agregar una sección de clave
de configuración ( ConnectionStrings ).
Se crea un nuevo par clave-valor de configuración que representa el proveedor de conexión de base de datos
(excepto para CUSTOMCONNSTR_ , que no tiene ningún proveedor indicado).

ENTRADA DE CONFIGURACIÓN DEL


CLAVE DE VARIABLE DE ENTORNO CLAVE DE CONFIGURACIÓN CONVERTIDA PROVEEDOR

CUSTOMCONNSTR_<KEY> ConnectionStrings:<KEY> Entrada de configuración no creada.


ENTRADA DE CONFIGURACIÓN DEL
CLAVE DE VARIABLE DE ENTORNO CLAVE DE CONFIGURACIÓN CONVERTIDA PROVEEDOR

MYSQLCONNSTR_<KEY> ConnectionStrings:<KEY> Clave:


ConnectionStrings:
<KEY>_ProviderName
:
Valor: MySql.Data.MySqlClient

SQLAZURECONNSTR_<KEY> ConnectionStrings:<KEY> Clave:


ConnectionStrings:
<KEY>_ProviderName
:
Valor: System.Data.SqlClient

SQLCONNSTR_<KEY> ConnectionStrings:<KEY> Clave:


ConnectionStrings:
<KEY>_ProviderName
:
Valor: System.Data.SqlClient

Proveedor de configuración de archivo


FileConfigurationProvider es la clase base para cargar la configuración del sistema de archivos. Los proveedores de
configuración siguientes están dedicados a determinados tipos de archivo:
Proveedor de configuración INI
Proveedor de configuración JSON
Proveedor de configuración XML
Proveedor de configuración de INI
IniConfigurationProvider carga la configuración desde pares clave-valor de archivo INI en tiempo de ejecución.
Para activar la configuración de archivo INI, llame al método de extensión AddIniFile en una instancia de
ConfigurationBuilder.
Los dos puntos se pueden usar como un delimitador de sección en la configuración de archivo INI.
Las sobrecargas permiten especificar:
Si el archivo es opcional.
Si la configuración se recarga si el archivo cambia.
IFileProvider que se usa para acceder al archivo.
Al llamar a CreateDefaultBuilder , llame a UseConfiguration con la configuración:
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}

public static IWebHostBuilder CreateWebHostBuilder(string[] args)


{
var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddIniFile("config.ini", optional: true, reloadOnChange: true)
.Build();

return WebHost.CreateDefaultBuilder(args)
.UseConfiguration(config)
.UseStartup<Startup>();
}
}

Al crear directamente un WebHostBuilder, llame a UseConfiguration con la configuración:


Aplique la configuración a WebHostBuilder con el método UseConfiguration :

var config = new ConfigurationBuilder()


.SetBasePath(Directory.GetCurrentDirectory())
.AddIniFile("config.ini", optional: true, reloadOnChange: true)
.Build();

var host = new WebHostBuilder()


.UseConfiguration(config)
.UseKestrel()
.UseStartup<Startup>();

Un ejemplo genérico de un archivo de configuración INI:

[section0]
key0=value
key1=value

[section1]
subsection:key=value

[section2:subsection0]
key=value

[section2:subsection1]
key=value

El archivo de configuración anterior carga las siguientes claves con value :


section0:key0
section0:key1
section1:subsection:key
section2:subsection0:key
section2:subsection1:key
Proveedor de configuración JSON
JsonConfigurationProvider carga la configuración desde pares clave-valor de archivos JSON en tiempo de
ejecución.
Para activar la configuración de archivo JSON, llame al método de extensión AddJsonFile en una instancia de
ConfigurationBuilder.
Las sobrecargas permiten especificar:
Si el archivo es opcional.
Si la configuración se recarga si el archivo cambia.
IFileProvider que se usa para acceder al archivo.
AddJsonFile se llama automáticamente dos veces al inicializar un nuevo WebHostBuilder con
CreateDefaultBuilder. Se llama al método para cargar la configuración desde:
appsettings.json – Este archivo se lee primero. La versión del entorno del archivo puede invalidar los valores que
proporciona el archivo appsettings.json.
appsettings.<Environment>.json – La versión del entorno del archivo se carga en función de
IHostingEnvironment.EnvironmentName.
Para más información, consulte Host web: Configuración de un host.
CreateDefaultBuilder también carga:
Variables de entorno.
Secretos de usuario (Administrador de secretos) (en el entorno de desarrollo).
Argumentos de la línea de comandos.
El proveedor de configuración de JSON se establece en primer lugar. Por tanto, los secretos de usuario, las
variables de entorno y los argumentos de la línea de comandos invalidan la configuración establecida por los
archivos appsettings.
Puede llamar directamente al método de extensión AddJsonFile en una instancia de ConfigurationBuilder.
Al llamar a CreateDefaultBuilder , llame a UseConfiguration con la configuración:

public class Program


{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}

public static IWebHostBuilder CreateWebHostBuilder(string[] args)


{
var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("config.json", optional: true, reloadOnChange: true)
.Build();

return WebHost.CreateDefaultBuilder(args)
.UseConfiguration(config)
.UseStartup<Startup>();
}
}

Al crear directamente un WebHostBuilder, llame a UseConfiguration con la configuración:


Aplique la configuración a WebHostBuilder con el método UseConfiguration :
var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("config.json", optional: true, reloadOnChange: true)
.Build();

var host = new WebHostBuilder()


.UseConfiguration(config)
.UseKestrel()
.UseStartup<Startup>();

Ejemplo
La aplicación de ejemplo 2.x aprovecha el método de conveniencia estático CreateDefaultBuilder para crear el host,
que incluye dos llamadas a AddJsonFile . La configuración se carga desde appsettings.json y appsettings.
<Environment>.json.
La aplicación de ejemplo 1.x llama a AddJsonFile dos veces en un ConfigurationBuilder . La configuración se carga
desde appsettings.json y appsettings.<Environment>.json.
1. Ejecute la aplicación de ejemplo. Abra un explorador a la aplicación en http://localhost:5000 .
2. Observe que la salida contiene los pares clave-valor para la configuración que se muestra en la tabla según el
entorno. Las claves de configuración de registro usan dos puntos ( : ) como separador jerárquico.

KEY VALOR DE DESARROLLO VALOR DE PRODUCCIÓN

Logging:LogLevel:System Información Información

Logging:LogLevel:Microsoft Información Información

Logging:LogLevel:Default Depuración Error

AllowedHosts * *

Proveedor de configuración XML


XmlConfigurationProvider carga la configuración desde pares clave-valor de archivo XML en tiempo de ejecución.
Para activar la configuración de archivo XML, llame al método de extensión AddXmlFile en una instancia de
ConfigurationBuilder.
Las sobrecargas permiten especificar:
Si el archivo es opcional.
Si la configuración se recarga si el archivo cambia.
IFileProvider que se usa para acceder al archivo.
El nodo raíz del archivo de configuración se omite cuando se crean los pares clave-valor de configuración. No
especifique una definición de tipo de documento (DTD ) ni el espacio de nombres en el archivo.
Al llamar a CreateDefaultBuilder , llame a UseConfiguration con la configuración:
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}

public static IWebHostBuilder CreateWebHostBuilder(string[] args)


{
var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddXmlFile("config.xml", optional: true, reloadOnChange: true)
.Build();

return WebHost.CreateDefaultBuilder(args)
.UseConfiguration(config)
.UseStartup<Startup>();
}
}

Al crear directamente un WebHostBuilder, llame a UseConfiguration con la configuración:


Aplique la configuración a WebHostBuilder con el método UseConfiguration :

var config = new ConfigurationBuilder()


.SetBasePath(Directory.GetCurrentDirectory())
.AddXmlFile("config.xml", optional: true, reloadOnChange: true)
.Build();

var host = new WebHostBuilder()


.UseConfiguration(config)
.UseKestrel()
.UseStartup<Startup>();

Los archivos de configuración XML pueden usar distintos nombres de elemento para las secciones repetidas:

<?xml version="1.0" encoding="UTF-8"?>


<configuration>
<section0>
<key0>value</key0>
<key1>value</key1>
</section0>
<section1>
<key0>value</key0>
<key1>value</key1>
</section1>
</configuration>

El archivo de configuración anterior carga las siguientes claves con value :


section0:key0
section0:key1
section1:key0
section1:key1
Los elementos de repetición que usan el mismo nombre de elemento funcionan si el atributo name se usa para
distinguir los elementos:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<section name="section0">
<key name="key0">value</key>
<key name="key1">value</key>
</section>
<section name="section1">
<key name="key0">value</key>
<key name="key1">value</key>
</section>
</configuration>

El archivo de configuración anterior carga las siguientes claves con value :


section:section0:key:key0
section:section0:key:key1
section:section1:key:key0
section:section1:key:key1
Los atributos se pueden usar para suministrar valores:

<?xml version="1.0" encoding="UTF-8"?>


<configuration>
<key attribute="value" />
<section>
<key attribute="value" />
</section>
</configuration>

El archivo de configuración anterior carga las siguientes claves con value :


key:attribute
section:key:attribute

Proveedor de configuración de clave por archivo


KeyPerFileConfigurationProvider usa los archivos de un directorio como pares clave-valor de configuración. La
clave es el nombre de archivo. El valor contiene el contenido del archivo. El proveedor de configuración de clave
por archivo se usa en escenarios de hospedaje de Docker.
Para activar la configuración de clave por archivo, llame al método de extensión AddKeyPerFile en una instancia de
ConfigurationBuilder. La ruta de acceso directoryPath a los archivos debe ser una ruta de acceso absoluta.
Las sobrecargas permiten especificar:
Un delegado Action<KeyPerFileConfigurationSource> que configura el origen.
Si el directorio es opcional y la ruta de acceso al directorio.
Al llamar a CreateDefaultBuilder , llame a UseConfiguration con la configuración:
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}

public static IWebHostBuilder CreateWebHostBuilder(string[] args)


{
var path = Path.Combine(Directory.GetCurrentDirectory(), "path/to/files");
var config = new ConfigurationBuilder()
.AddKeyPerFile(directoryPath: path, optional: true)
.Build();

return WebHost.CreateDefaultBuilder(args)
.UseConfiguration(config)
.UseStartup<Startup>();
}
}

Al crear directamente un WebHostBuilder, llame a UseConfiguration con la configuración:

var path = Path.Combine(Directory.GetCurrentDirectory(), "path/to/files");


var config = new ConfigurationBuilder()
.AddKeyPerFile(directoryPath: path, optional: true)
.Build();

var host = new WebHostBuilder()


.UseConfiguration(config)
.UseKestrel()
.UseStartup<Startup>();

Proveedor de configuración de memoria


MemoryConfigurationProvider usa una colección en memoria como pares clave-valor de configuración.
Para activar la configuración de colección en memoria, llame al método de extensión AddInMemoryCollection en
una instancia de ConfigurationBuilder.
El proveedor de configuración se puede inicializar con un IEnumerable<KeyValuePair<String,String>> .
Al llamar a CreateDefaultBuilder , llame a UseConfiguration con la configuración:
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}

public static IWebHostBuilder CreateWebHostBuilder(string[] args)


{
var dict = new Dictionary<string, string>
{
{"MemoryCollectionKey1", "value1"},
{"MemoryCollectionKey2", "value2"}
};

var config = new ConfigurationBuilder()


.AddInMemoryCollection(dict)
.Build();

return WebHost.CreateDefaultBuilder(args)
.UseConfiguration(config)
.UseStartup<Startup>();
}
}

Al crear directamente un WebHostBuilder, llame a UseConfiguration con la configuración:


Aplique la configuración a WebHostBuilder con el método UseConfiguration :

var dict = new Dictionary<string, string>


{
{"MemoryCollectionKey1", "value1"},
{"MemoryCollectionKey2", "value2"}
};

var config = new ConfigurationBuilder()


.AddInMemoryCollection(dict)
.Build();

var host = new WebHostBuilder()


.UseConfiguration(config)
.UseKestrel()
.UseStartup<Startup>();

GetValue
ConfigurationBinder.GetValue<T> extrae un valor de la configuración con una clave especificada y lo convierte al
tipo especificado. Una sobrecarga permite proporcionar un valor predeterminado si no se encuentra la clave.
El ejemplo siguiente extrae el valor de cadena desde la configuración con la clave NumberKey , escribe el valor como
int y almacena el valor en la variable intValue . Si NumberKey no se encuentra en las claves de configuración,
intValue recibe el valor predeterminado de 99 :

var intValue = config.GetValue<int>("NumberKey", 99);

GetSection, GetChildren y Exists


Para los ejemplos siguientes, considere el siguiente archivo JSON. Se encuentran cuatro claves en dos secciones,
una de las cuales incluye un par de subsecciones:
{
"section0": {
"key0": "value",
"key1": "value"
},
"section1": {
"key0": "value",
"key1": "value"
},
"section2": {
"subsection0" : {
"key0": "value",
"key1": "value"
},
"subsection1" : {
"key0": "value",
"key1": "value"
}
}
}

Cuando el archivo se lee en la configuración, se crean las siguientes claves jerárquicas únicas para contener los
valores de configuración:
section0:key0
section0:key1
section1:key0
section1:key1
section2:subsection0:key0
section2:subsection0:key1
section2:subsection1:key0
section2:subsection1:key1
GetSection
IConfiguration.GetSection extrae una subsección de la configuración con la clave de subsección especificada.
Para devolver un IConfigurationSection que contiene los pares clave-valor en section1 , llame a GetSection y
suministre el nombre de la sección:

var configSection = _config.GetSection("section1");

De forma similar, para obtener los valores de las claves de section2:subsection0 , llame a GetSection y suministre
la ruta de acceso a la sección:

var configSection = _config.GetSection("section2:subsection0");

GetSection nunca devuelve null . Si no se encuentra una sección que coincida, se devuelve una
IConfigurationSection vacía.

GetChildren
Una llamada a IConfiguration.GetChildren en section2 obtiene una IEnumerable<IConfigurationSection> que
incluye:
subsection0
subsection1
var configSection = _config.GetSection("section2");

var children = configSection.GetChildren();

Existe
Use ConfigurationExtensions.Exists para determinar si existe una sección de configuración:

var sectionExists = _config.GetSection("section2:subsection2").Exists();

Dados los datos de ejemplo, sectionExists es false porque no hay una sección section2:subsection2 en los
datos de configuración.

Enlace a una clase


La configuración se puede enlazar a clases que representan grupos de configuraciones relacionadas a través del
patrón de opciones. Para obtener más información, vea Patrón de opciones en ASP.NET Core.
Los valores de configuración se devuelven como cadenas, pero llamar a Bind permite la construcción de objetos
POCO.
La aplicación de ejemplo contiene un modelo Starship (Models/Starship.cs):

public class Starship


{
public string Name { get; set; }
public string Registry { get; set; }
public string Class { get; set; }
public decimal Length { get; set; }
public bool Commissioned { get; set; }
}

public class Starship


{
public string Name { get; set; }
public string Registry { get; set; }
public string Class { get; set; }
public decimal Length { get; set; }
public bool Commissioned { get; set; }
}

La sección starship del archivo starship.json crea la configuración cuando la aplicación de ejemplo usa el
proveedor de configuración JSON para cargar la configuración:

{
"starship": {
"name": "USS Enterprise",
"registry": "NCC-1701",
"class": "Constitution",
"length": 304.8,
"commissioned": false
},
"trademark": "Paramount Pictures Corp. http://www.paramount.com"
}
{
"starship": {
"name": "USS Enterprise",
"registry": "NCC-1701",
"class": "Constitution",
"length": 304.8,
"commissioned": false
},
"trademark": "Paramount Pictures Corp. http://www.paramount.com"
}

Se crean los siguientes pares clave-valor de configuración:

KEY VALOR

starship:name USS Enterprise

starship:registry NCC-1701

starship:class Constitution

starship:length 304.8

starship:commissioned False

trademark Paramount Pictures Corp. http://www.paramount.com

La aplicación de ejemplo llama a GetSection con la clave starship . Los pares clave-valor starship están aislados.
El método Bind se llama en la subsección que pasa una instancia de la clase Starship . Después de enlazar los
valores de instancia, la instancia está asignada a una propiedad para la representación:

var starship = new Starship();


_config.GetSection("starship").Bind(starship);
Starship = starship;

var starship = new Starship();


_config.GetSection("starship").Bind(starship);
viewModel.Starship = starship;

Enlazar a un gráfico de objetos


Bind es capaz de enlazar todo un gráfico de objetos POCO.
El ejemplo contiene un modelo TvShow cuyo gráfico de objetos incluye las clases Metadata y Actors
(Models/TvShow.cs):
public class TvShow
{
public Metadata Metadata { get; set; }
public Actors Actors { get; set; }
public string Legal { get; set; }
}

public class Metadata


{
public string Series { get; set; }
public string Title { get; set; }
public DateTime AirDate { get; set; }
public int Episodes { get; set; }
}

public class Actors


{
public string Names { get; set; }
}

public class TvShow


{
public Metadata Metadata { get; set; }
public Actors Actors { get; set; }
public string Legal { get; set; }
}

public class Metadata


{
public string Series { get; set; }
public string Title { get; set; }
public DateTime AirDate { get; set; }
public int Episodes { get; set; }
}

public class Actors


{
public string Names { get; set; }
}

La aplicación de ejemplo tiene un archivo tvshow.xml que contiene los datos de configuración:

<?xml version="1.0" encoding="UTF-8"?>


<configuration>
<tvshow>
<metadata>
<series>Dr. Who</series>
<title>The Sun Makers</title>
<airdate>11/26/1977</airdate>
<episodes>4</episodes>
</metadata>
<actors>
<names>Tom Baker, Louise Jameson, John Leeson</names>
</actors>
<legal>(c)1977 BBC https://www.bbc.co.uk/programmes/b006q2x0</legal>
</tvshow>
</configuration>
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<tvshow>
<metadata>
<series>Dr. Who</series>
<title>The Sun Makers</title>
<airdate>11/26/1977</airdate>
<episodes>4</episodes>
</metadata>
<actors>
<names>Tom Baker, Louise Jameson, John Leeson</names>
</actors>
<legal>(c)1977 BBC https://www.bbc.co.uk/programmes/b006q2x0</legal>
</tvshow>
</configuration>

Configuración está enlazada a todo el gráfico de objetos TvShow con el método Bind . La instancia enlazada se
asigna a una propiedad para la representación:

var tvShow = new TvShow();


_config.GetSection("tvshow").Bind(tvShow);
TvShow = tvShow;

var tvShow = new TvShow();


_config.GetSection("tvshow").Bind(tvShow);
viewModel.TvShow = tvShow;

ConfigurationBinder.Get<T> enlaza y devuelve el tipo especificado. Get<T> es más conveniente que usar Bind . El
código siguiente muestra cómo usar Get<T> con el ejemplo anterior, lo que permite que la instancia enlazada se
asigne directamente a la propiedad que se usa para la representación:

TvShow = _config.GetSection("tvshow").Get<TvShow>();

viewModel.TvShow = _config.GetSection("tvshow").Get<TvShow>();

Enlace de una matriz a una clase


La aplicación de ejemplo muestra los conceptos que se explican en esta sección.
Bind admite enlazar matrices a objetos con los índices de matriz en las claves de configuración. Cualquier formato
de matriz que expone un segmento de clave numérica ( :0: , :1: , … :{n}: ) es capaz de enlazar una matriz a una
matriz de clase POCO.

NOTE
El enlace se proporciona por convención. Los proveedores de configuración personalizados no son necesarios para
implementar el enlace de matriz.

Procesamiento de matriz en memoria


Considere los valores y las claves de configuración que se muestran en esta tabla.
KEY VALOR

array:0 value0

array:1 value1

array:2 value2

array:4 value4

array:5 value5

Estas claves y valores se cargan en la aplicación de ejemplo a través del proveedor de configuración de memoria:

public class Program


{
public static Dictionary<string, string> arrayDict = new Dictionary<string, string>
{
{"array:entries:0", "value0"},
{"array:entries:1", "value1"},
{"array:entries:2", "value2"},
{"array:entries:4", "value4"},
{"array:entries:5", "value5"}
};

public static void Main(string[] args)


{
CreateWebHostBuilder(args).Build().Run();
}

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>


WebHost.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((hostingContext, config) =>
{
config.SetBasePath(Directory.GetCurrentDirectory());
config.AddInMemoryCollection(arrayDict);
config.AddJsonFile("json_array.json", optional: false, reloadOnChange: false);
config.AddJsonFile("starship.json", optional: false, reloadOnChange: false);
config.AddXmlFile("tvshow.xml", optional: false, reloadOnChange: false);
config.AddEFConfiguration(options => options.UseInMemoryDatabase("InMemoryDb"));
config.AddCommandLine(args);
})
.UseStartup<Startup>();
}
public class Startup
{
public Startup(IHostingEnvironment env)
{
var arrayDict = new Dictionary<string, string>
{
{"array:entries:0", "value0"},
{"array:entries:1", "value1"},
{"array:entries:2", "value2"},
{"array:entries:4", "value4"},
{"array:entries:5", "value5"}
};

var builder = new ConfigurationBuilder()


.SetBasePath(Directory.GetCurrentDirectory())
.AddInMemoryCollection(arrayDict)
.AddJsonFile("json_array.json", optional: false, reloadOnChange: false)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true,
reloadOnChange: true)
.AddEnvironmentVariables()
.AddJsonFile("starship.json", optional: false, reloadOnChange: false)
.AddXmlFile("tvshow.xml", optional: false, reloadOnChange: false)
.AddEFConfiguration(options => options.UseInMemoryDatabase("InMemoryDb"))
.AddCommandLine(Program.Args);

Configuration = builder.Build();
}

La matriz omite un valor de índice #3. El enlazador de configuración no es capaz de enlazar los valores NULL ni de
crear entradas NULL en los objetos enlazados, lo que queda claro cuando se muestra el resultado de enlazar esta
matriz a un objeto.
En la aplicación de ejemplo, hay disponible una clase POCO para almacenar los datos de configuración enlazados:

public class ArrayExample


{
public string[] Entries { get; set; }
}

public class ArrayExample


{
public string[] Entries { get; set; }
}

Los datos de configuración están enlazados al objeto:

var arrayExample = new ArrayExample();


_config.GetSection("array").Bind(arrayExample);

También se puede usar la sintaxis de ConfigurationBinder.Get<T> , lo que genera un código más compacto:

ArrayExample = _config.GetSection("array").Get<ArrayExample>();

viewModel.ArrayExample = _config.GetSection("array").Get<ArrayExample>();

El objeto enlazado, una instancia de ArrayExample , recibe los datos de la matriz desde la configuración.
ÍNDICE DE ARRAYEXAMPLES.ENTRIES VALOR DE ARRAYEXAMPLES.ENTRIES

0 value0

1 value1

2 value2

3 value4

4 value5

El índice #3 en el objeto enlazado contiene los datos de configuración para la clave de configuración array:4 y su
valor de value4 . Cuando se enlazan datos de configuración que contienen una matriz, los índices de la matriz en
las claves de configuración solo se usan para iterar los datos de configuración al crear el objeto. No se puede
conservar un valor NULL en los datos de configuración y no se crea una entrada con valores NULL en un objeto
enlazado cuando una matriz en las claves de configuración omite uno o más índices.
El elemento de configuración omitido para el índice #3 se puede proporcionar antes del enlace a la instancia
ArrayExamples por cualquier proveedor de configuración que genera el par clave-valor correcto en la
configuración. Si el ejemplo incluía un proveedor de configuración JSON adicional con el par clave-valor omitido,
ArrayExamples.Entries coincide con la matriz de configuración completa:

missing_value.json:

{
"array:entries:3": "value3"
}

En ConfigureAppConfiguration :

config.AddJsonFile("missing_value.json", optional: false, reloadOnChange: false);

En el constructor Startup :

.AddJsonFile("missing_value.json", optional: false, reloadOnChange: false);

El par clave-valor que se muestra en la tabla se carga en la configuración.

KEY VALOR

array:entries:3 value3

Si la instancia de clase ArrayExamples se enlaza después de que el proveedor de configuración JSON incluye la
entrada para el índice #3, la matriz ArrayExamples.Entries incluye el valor.

ÍNDICE DE ARRAYEXAMPLES.ENTRIES VALOR DE ARRAYEXAMPLES.ENTRIES

0 value0

1 value1
ÍNDICE DE ARRAYEXAMPLES.ENTRIES VALOR DE ARRAYEXAMPLES.ENTRIES

2 value2

3 value3

4 value4

5 value5

Procesamiento de matriz JSON


Si un archivo JSON contiene una matriz, se crean claves de configuración para los elementos de la matriz con un
índice de sección basado en cero. En el siguiente archivo de configuración, subsection es una matriz:

{
"json_array": {
"key": "valueA",
"subsection": [
"valueB",
"valueC",
"valueD"
]
}
}

{
"json_array": {
"key": "valueA",
"subsection": [
"valueB",
"valueC",
"valueD"
]
}
}

El proveedor de configuración JSON lee los datos de configuración en los siguientes pares clave-valor:

KEY VALOR

json_array:key valueA

json_array:subsection:0 valueB

json_array:subsection:1 valueC

json_array:subsection:2 valueD

En la aplicación de ejemplo, la clase POCO siguiente está disponible para enlazar los pares clave-valor de
configuración:
public class JsonArrayExample
{
public string Key { get; set; }
public string[] Subsection { get; set; }
}

public class JsonArrayExample


{
public string Key { get; set; }
public string[] Subsection { get; set; }
}

Después del enlace, JsonArrayExample.Key contiene el valor valueA . Los valores de la subsección se almacenan en
la propiedad de la matriz POCO, Subsection .

ÍNDICE DE JSONARRAYEXAMPLE.SUBSECTION VALOR DE JSONARRAYEXAMPLE.SUBSECTION

0 valueB

1 valueC

2 valueD

Proveedores de configuración personalizada


La aplicación de ejemplo muestra cómo crear un proveedor de configuración básica que lee los pares clave-valor
de configuración desde una base de datos mediante Entity Framework (EF ).
El proveedor tiene las siguientes características:
La base de datos en memoria de EF se usa para fines de demostración. Para usar una base de datos que
requiere una cadena de conexión, implemente un ConfigurationBuilder secundario para suministrar la cadena
de conexión desde otro proveedor de configuración.
El proveedor lee una tabla de base de datos en la configuración en el inicio. El proveedor no consulta la base de
datos por clave.
La función de recarga en cambio no se implementa, por lo que actualizar la base de datos después del inicio de
la aplicación no afecta a la configuración de la aplicación.
Defina una entidad EFConfigurationValue para almacenar los valores de configuración en la base de datos.
Models/EFConfigurationValue.cs:

public class EFConfigurationValue


{
public string Id { get; set; }
public string Value { get; set; }
}

public class EFConfigurationValue


{
public string Id { get; set; }
public string Value { get; set; }
}
Agregue un EFConfigurationContext para almacenar y tener acceso a los valores configurados.
EFConfigurationProvider/EFConfigurationContext.cs:

public class EFConfigurationContext : DbContext


{
public EFConfigurationContext(DbContextOptions options) : base(options)
{
}

public DbSet<EFConfigurationValue> Values { get; set; }


}

public class EFConfigurationContext : DbContext


{
public EFConfigurationContext(DbContextOptions options) : base(options)
{
}

public DbSet<EFConfigurationValue> Values { get; set; }


}

Cree una clase que implemente IConfigurationSource.


EFConfigurationProvider/EFConfigurationSource.cs:

public class EFConfigurationSource : IConfigurationSource


{
private readonly Action<DbContextOptionsBuilder> _optionsAction;

public EFConfigurationSource(Action<DbContextOptionsBuilder> optionsAction)


{
_optionsAction = optionsAction;
}

public IConfigurationProvider Build(IConfigurationBuilder builder)


{
return new EFConfigurationProvider(_optionsAction);
}
}

public class EFConfigurationSource : IConfigurationSource


{
private readonly Action<DbContextOptionsBuilder> _optionsAction;

public EFConfigurationSource(Action<DbContextOptionsBuilder> optionsAction)


{
_optionsAction = optionsAction;
}

public IConfigurationProvider Build(IConfigurationBuilder builder)


{
return new EFConfigurationProvider(_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.
EFConfigurationProvider/EFConfigurationProvider.cs:
public class EFConfigurationProvider : ConfigurationProvider
{
public EFConfigurationProvider(Action<DbContextOptionsBuilder> optionsAction)
{
OptionsAction = optionsAction;
}

Action<DbContextOptionsBuilder> OptionsAction { get; }

// Load config data from EF DB.


public override void Load()
{
var builder = new DbContextOptionsBuilder<EFConfigurationContext>();

OptionsAction(builder);

using (var dbContext = new EFConfigurationContext(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(


EFConfigurationContext dbContext)
{
// Quotes (c)2005 Universal Pictures: Serenity
// https://www.uphe.com/movies/serenity
var configValues = new Dictionary<string, string>
{
{ "quote1", "I aim to misbehave." },
{ "quote2", "I swallowed a bug." },
{ "quote3", "You can't stop the signal, Mal." }
};

dbContext.Values.AddRange(configValues
.Select(kvp => new EFConfigurationValue
{
Id = kvp.Key,
Value = kvp.Value
})
.ToArray());

dbContext.SaveChanges();

return configValues;
}
}
public class EFConfigurationProvider : ConfigurationProvider
{
public EFConfigurationProvider(Action<DbContextOptionsBuilder> optionsAction)
{
OptionsAction = optionsAction;
}

Action<DbContextOptionsBuilder> OptionsAction { get; }

// Load config data from EF DB.


public override void Load()
{
var builder = new DbContextOptionsBuilder<EFConfigurationContext>();

OptionsAction(builder);

using (var dbContext = new EFConfigurationContext(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(


EFConfigurationContext dbContext)
{
// Quotes (c)2005 Universal Pictures: Serenity
// https://www.uphe.com/movies/serenity
var configValues = new Dictionary<string, string>
{
{ "quote1", "I aim to misbehave." },
{ "quote2", "I swallowed a bug." },
{ "quote3", "You can't stop the signal, Mal." }
};

dbContext.Values.AddRange(configValues
.Select(kvp => new EFConfigurationValue
{
Id = kvp.Key,
Value = kvp.Value
})
.ToArray());

dbContext.SaveChanges();

return configValues;
}
}

Un método de extensión AddEFConfiguration permite agregar el origen de configuración a un


ConfigurationBuilder .

Extensions/EntityFrameworkExtensions.cs:
public static class EntityFrameworkExtensions
{
public static IConfigurationBuilder AddEFConfiguration(
this IConfigurationBuilder builder,
Action<DbContextOptionsBuilder> optionsAction)
{
return builder.Add(new EFConfigurationSource(optionsAction));
}
}

public static class EntityFrameworkExtensions


{
public static IConfigurationBuilder AddEFConfiguration(
this IConfigurationBuilder builder,
Action<DbContextOptionsBuilder> optionsAction)
{
return builder.Add(new EFConfigurationSource(optionsAction));
}
}

En el código siguiente se muestra cómo puede usar el EFConfigurationProvider personalizado en Program.cs:

public class Program


{
public static Dictionary<string, string> arrayDict = new Dictionary<string, string>
{
{"array:entries:0", "value0"},
{"array:entries:1", "value1"},
{"array:entries:2", "value2"},
{"array:entries:4", "value4"},
{"array:entries:5", "value5"}
};

public static void Main(string[] args)


{
CreateWebHostBuilder(args).Build().Run();
}

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>


WebHost.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((hostingContext, config) =>
{
config.SetBasePath(Directory.GetCurrentDirectory());
config.AddInMemoryCollection(arrayDict);
config.AddJsonFile("json_array.json", optional: false, reloadOnChange: false);
config.AddJsonFile("starship.json", optional: false, reloadOnChange: false);
config.AddXmlFile("tvshow.xml", optional: false, reloadOnChange: false);
config.AddEFConfiguration(options => options.UseInMemoryDatabase("InMemoryDb"));
config.AddCommandLine(args);
})
.UseStartup<Startup>();
}
public class Startup
{
public Startup(IHostingEnvironment env)
{
var arrayDict = new Dictionary<string, string>
{
{"array:entries:0", "value0"},
{"array:entries:1", "value1"},
{"array:entries:2", "value2"},
{"array:entries:4", "value4"},
{"array:entries:5", "value5"}
};

var builder = new ConfigurationBuilder()


.SetBasePath(Directory.GetCurrentDirectory())
.AddInMemoryCollection(arrayDict)
.AddJsonFile("json_array.json", optional: false, reloadOnChange: false)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true,
reloadOnChange: true)
.AddEnvironmentVariables()
.AddJsonFile("starship.json", optional: false, reloadOnChange: false)
.AddXmlFile("tvshow.xml", optional: false, reloadOnChange: false)
.AddEFConfiguration(options => options.UseInMemoryDatabase("InMemoryDb"))
.AddCommandLine(Program.Args);

Configuration = builder.Build();
}

Acceso a la configuración durante el inicio


Inserte en el constructor Startup para acceder a los valores de configuración en
IConfiguration
Startup.ConfigureServices . Para acceder a la configuración en Startup.Configure , inserte IConfiguration
directamente en el método o use la instancia desde el constructor:

public class Startup


{
private readonly IConfiguration _config;

public Startup(IConfiguration config)


{
_config = config;
}

public void ConfigureServices(IServiceCollection services)


{
var value = _config["key"];
}

public void Configure(IApplicationBuilder app, IConfiguration config)


{
var value = config["key"];
}
}

Para un ejemplo de cómo acceder a la configuración a través de métodos de conveniencia de inicio, consulte Inicio
de la aplicación: Métodos de conveniencia.

Acceso a la configuración en una página de Razor Pages o en una vista


de MVC.
Para obtener acceso a los valores de configuración en una página de Razor Pages 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 en 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 value for 'key': @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 value for 'key': @Configuration["key"]</p>
</body>
</html>

Agregar 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, vea Mejorar una aplicación
desde un ensamblado externo en ASP.NET Core con IHostingStartup.

Recursos adicionales
Patrón de opciones en ASP.NET Core
Deep Dive into Microsoft Configuration (Profundización en la configuración de Microsoft)
Configuración en ASP.NET Core
07/09/2018 • 57 minutes to read • Edit Online

Por Luke Latham


La configuración de la aplicación en ASP.NET Core se basa en pares clave-valor establecidos por
proveedores de configuración. Los proveedores de configuración leen los datos de configuración en
los pares clave-valor de distintos orígenes de configuración:
Azure Key Vault
Argumentos de la línea de comandos
Proveedores personalizados (instalados o creados)
Archivos de directorio
Variables de entorno
Objetos de .NET en memoria
Archivos de configuración
Azure Key Vault
Argumentos de la línea de comandos
Proveedores personalizados (instalados o creados)
Variables de entorno
Objetos de .NET en memoria
Archivos de configuración
Argumentos de la línea de comandos
Proveedores personalizados (instalados o creados)
Variables de entorno
Objetos de .NET en memoria
Archivos de configuración
El patrón de opciones es una extensión de los conceptos de configuración que se describen en este
tema. Las opciones usan clases para representar grupos de configuraciones relacionadas. Para más
información sobre cómo usar el patrón de opciones, consulte Patrón de opciones en ASP.NET Core.
Vea o descargue el código de ejemplo (cómo descargarlo)
Los ejemplos proporcionados en este tema dependen de lo siguiente:
Establecer la ruta base de la aplicación con SetBasePath. SetBasePath está disponible para una
aplicación mediante una referencia al paquete Microsoft.Extensions.Configuration.FileExtensions.
Resolver las secciones de los archivos de configuración con GetSection. GetSection está disponible
para una aplicación mediante una referencia al paquete Microsoft.Extensions.Configuration.
Enlazar la configuración a clases .NET con Bind y Get<T>. Bind y Get<T> están disponibles para
una aplicación mediante una referencia al paquete Microsoft.Extensions.Configuration.Binder.
Get<T> está disponible en ASP.NET Core 1.1 o versiones posteriores.

Estos tres paquetes están incluidos en el metapaquete Microsoft.AspNetCore.App.


Estos tres paquetes están incluidos en el metapaquete Microsoft.AspNetCore.All.
Configuración de host frente a configuración de aplicación
Antes de configurar e iniciar la aplicación, se configura e inicia un host. El host es responsable de la
administración del inicio y la duración de la aplicación. Tanto la aplicación como el host se configuran
mediante los proveedores de configuración que se describen en este tema. Los pares clave-valor de la
configuración de host se vuelven parte de la configuración global de la aplicación. Para más
información sobre cómo se usan los proveedores de configuración cuando se compila el host y cómo
afectan los orígenes de configuración a la configuración del host, consulte Hospedaje en ASP.NET
Core.

Seguridad
Adopte estos procedimientos recomendados:
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 varios entornos y cómo administrar el almacenamiento
seguro de secretos de aplicación en el desarrollo con el Administrador de secretos (incluye consejos
sobre el uso de variables de entorno para almacenar información confidencial). El Administrador de
secretos usa el proveedor de configuración de archivo para almacenar secretos de usuario en un
archivo JSON del sistema local. El proveedor de configuración de archivo se describe más adelante en
este tema.
Azure Key Vault es una opción para el almacenamiento seguro de los secretos de aplicación. Para
obtener más información, vea Proveedor de configuración de Azure Key Vault en ASP.NET Core.

Datos de configuración jerárquica


La API de configuración es capaz de mantener los datos de configuración jerárquica al disminuir los
datos jerárquicos mediante el uso de un delimitador en las claves de configuración.
En el siguiente archivo JSON, hay cuatro claves en una estructura jerárquica de dos secciones:

{
"section0": {
"key0": "value",
"key1": "value"
},
"section1": {
"key0": "value",
"key1": "value"
}
}

Cuando el archivo se lee en la configuración, se crean claves únicas para mantener la estructura de
datos jerárquicos original del origen de configuración. Las secciones y claves se disminuyen con el uso
de dos puntos ( : ) para mantener la estructura original:
section0:key0
section0:key1
section1:key0
section1:key1
Los métodos GetSection y GetChildren están disponibles para aislar las secciones y los elementos
secundarios de una sección en los datos de configuración. Estos métodos se describen más adelante
en la sección GetSection, GetChildren y Exists.

Convenciones
Al iniciar la aplicación, los orígenes de configuración se leen en el orden en que se especifican sus
proveedores de configuración.
Los proveedores de configuración de archivo tienen la capacidad de recargar la configuración cuando
se modifica un archivo de configuración subyacente después del inicio de la aplicación. El proveedor
de configuración de archivo se describe más adelante en este tema.
IConfiguration está disponible en el contenedor Inserción de dependencias (DI) de la aplicación. Los
proveedores de configuración no pueden usar la inserción de dependencias, porque no está disponible
cuando el host los configura.
Las claves de configuración adoptan las convenciones siguientes:
Las claves no distinguen mayúsculas de minúsculas. Por ejemplo, ConnectionString y
connectionstring se consideran claves equivalente.
Si los mismos o distintos proveedores de configuración establecen un valor para la misma clave, el
último valor establecido en la clave es el valor que se usa.
Claves jerárquicas
Dentro de la API de configuración, un separador de dos puntos ( : ) funciona en todas las
plataformas.
En las variables de entorno, puede que un separador de dos puntos no funcione en todas las
plataformas. Un guion bajo doble ( __ ) es compatible con todas las plataformas y se
convierte en un signo de dos puntos.
En Azure Key Vault, las claves jerárquicas usan -- (dos guiones) como separador. Debe
proporcionar código para reemplazar los guiones por dos puntos cuando los secretos se
cargan en la configuración de la aplicación.
ConfigurationBinder admite enlazar matrices a objetos con los índices de matriz en las claves de
configuración. El enlace de matriz se describe en la sección Enlace de una matriz a una clase.
Los valores de configuración adoptan las convenciones siguientes:
Los valores son cadenas.
Los valores NULL no se pueden almacenar en la configuración ni enlazar a los objetos.

Proveedores
La siguiente tabla muestra los proveedores de configuración disponibles para las aplicaciones de
ASP.NET Core.

PROVEEDOR PROPORCIONA LA CONFIGURACIÓN DE…

Proveedor de configuración de Azure Key Vault (temas Azure Key Vault


de Seguridad)

Proveedor de configuración de línea de comandos Parámetros de la línea de comandos

Proveedor de configuración personalizada Origen personalizado


PROVEEDOR PROPORCIONA LA CONFIGURACIÓN DE…

Proveedor de configuración de variables de entorno Variables de entorno

Proveedor de configuración de archivo Archivos (INI, JSON, XML)

Proveedor de configuración de clave por archivo Archivos de directorio

Proveedor de configuración de memoria Colecciones en memoria

Secretos de usuario (Administrador de secretos) (temas Archivo en el directorio del perfil de usuario
de Seguridad)

PROVEEDOR PROPORCIONA LA CONFIGURACIÓN DE…

Proveedor de configuración de Azure Key Vault (temas Azure Key Vault


de Seguridad)

Proveedor de configuración de línea de comandos Parámetros de la línea de comandos

Proveedor de configuración personalizada Origen personalizado

Proveedor de configuración de variables de entorno Variables de entorno

Proveedor de configuración de archivo Archivos (INI, JSON, XML)

Proveedor de configuración de memoria Colecciones en memoria

Secretos de usuario (Administrador de secretos) (temas Archivo en el directorio del perfil de usuario
de Seguridad)

PROVEEDOR PROPORCIONA LA CONFIGURACIÓN DE…

Proveedor de configuración de línea de comandos Parámetros de la línea de comandos

Proveedor de configuración personalizada Origen personalizado

Proveedor de configuración de variables de entorno Variables de entorno

Proveedor de configuración de archivo Archivos (INI, JSON, XML)

Proveedor de configuración de memoria Colecciones en memoria

Secretos de usuario (Administrador de secretos) (temas Archivo en el directorio del perfil de usuario
de Seguridad)

Los orígenes de configuración se leen en el orden en que se especifican sus proveedores de


configuración en el inicio. En este tema, los proveedores de configuración se describen en orden
alfabético y no en el orden en que el código podría organizarlos. Ordene los proveedores de
configuración en el código para cumplir con sus prioridades relacionadas con los orígenes de
configuración subyacentes.
Esta es una secuencia típica de proveedores de configuración:
1. Archivos (appsettings.json, appsettings.<Environment>.json, donde <Environment> es el entorno de
hospedaje actual de la aplicación)
2. Secretos de usuario (Administrador de secretos) (solo en el entorno de desarrollo)
3. Variables de entorno
4. Argumentos de la línea de comandos
Una práctica común es colocar el proveedor de configuración de línea de comandos al final de en una
serie de proveedores para permitir que los argumentos de la línea de comandos invaliden la
configuración que establecieron los demás proveedores.
Esta secuencia de los proveedores se aplica al inicializar un nuevo WebHostBuilder con
CreateDefaultBuilder. Para más información, consulte Host web: Configuración de un host.
Llame a ConfigureAppConfiguration cuando cree el host web para especificar los proveedores de
configuración de la aplicación:

public class Program


{
public static Dictionary<string, string> arrayDict = new Dictionary<string, string>
{
{"array:entries:0", "value0"},
{"array:entries:1", "value1"},
{"array:entries:2", "value2"},
{"array:entries:4", "value4"},
{"array:entries:5", "value5"}
};

public static void Main(string[] args)


{
CreateWebHostBuilder(args).Build().Run();
}

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>


WebHost.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((hostingContext, config) =>
{
config.SetBasePath(Directory.GetCurrentDirectory());
config.AddInMemoryCollection(arrayDict);
config.AddJsonFile("json_array.json", optional: false, reloadOnChange: false);
config.AddJsonFile("starship.json", optional: false, reloadOnChange: false);
config.AddXmlFile("tvshow.xml", optional: false, reloadOnChange: false);
config.AddEFConfiguration(options => options.UseInMemoryDatabase("InMemoryDb"));
config.AddCommandLine(args);
})
.UseStartup<Startup>();
}

ConfigureAppConfiguration está disponible en ASP.NET Core 2.1 o versiones posteriores.


Esta secuencia de proveedores se puede crear para la aplicación (no para el host) con
ConfigurationBuilder y una llamada a su método Build en Startup :
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true,
reloadOnChange: true);

var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName));

if (appAssembly != null)
{
builder.AddUserSecrets(appAssembly, optional: true);
}

builder.AddEnvironmentVariables();

Configuration = builder.Build();
}

public IConfiguration Configuration { get; }

public void ConfigureServices(IServiceCollection services)


{
services.AddSingleton<IConfiguration>(Configuration);
}

En el ejemplo anterior, IHostingEnvironment proporciona el nombre del entorno (


env.EnvironmentName ) y el nombre del ensamblado de la aplicación ( env.ApplicationName ). Para
obtener más información, vea Usar varios entornos en ASP.NET Core.

Proveedor de configuración de línea de comandos


CommandLineConfigurationProvider carga la configuración de pares clave-valor de argumento de la
línea de comandos en tiempo de ejecución.
Para activar la configuración de línea de comandos, llame al método de extensión AddCommandLine
en una instancia de ConfigurationBuilder.
AddCommandLine se llama automáticamente cuando se inicializa un nuevo WebHostBuilder con
CreateDefaultBuilder. Para más información, consulte Host web: Configuración de un host.
CreateDefaultBuilder también carga:
Configuración opcional de appsettings.json y appsettings.< Environment>.json.
Secretos de usuario (Administrador de secretos) (en el entorno de desarrollo).
Variables de entorno.
CreateDefaultBuilder agrega el proveedor de configuración de línea de comandos al final. Los
argumentos de la línea de comandos que se pasan en tiempo de ejecución invalidan la configuración
establecida por los otros proveedores.
CreateDefaultBuilderactúa cuando se construye el host. Por tanto, la configuración de línea de
comandos activada por CreateDefaultBuilder puede afectar la manera en que se configura el host.
Al crear el host manualmente y no mediante una llamada a CreateDefaultBuilder , llame al método de
extensión AddCommandLine en una instancia de ConfigurationBuilder:
Para activar la configuración de línea de comandos, llame al método de extensión AddCommandLine
en una instancia de ConfigurationBuilder.
Llame en último lugar al proveedor para permitir que los argumentos de la línea de comandos que se
pasan en tiempo de ejecución invaliden la configuración establecida por los otros proveedores de
configuración.
Aplique la configuración a WebHostBuilder con el método UseConfiguration:

var config = new ConfigurationBuilder()


.AddCommandLine(args)
.Build();

var host = new WebHostBuilder()


.UseConfiguration(config)
.UseKestrel()
.UseStartup<Startup>();

Ejemplo
La aplicación de ejemplo 2.x aprovecha el método de conveniencia estático CreateDefaultBuilder para
crear el host, que incluye una llamada a AddCommandLine.
La aplicación de ejemplo 1.x llama a AddCommandLine en ConfigurationBuilder.
1. Abra un símbolo del sistema en el directorio del proyecto.
2. Proporcione un argumento de la línea de comandos para el comando dotnet run ,
dotnet run CommandLineKey=CommandLineValue .
3. Una vez que se ejecuta la aplicación, abra un explorador a la aplicación en http://localhost:5000 .
4. Observe que la salida contiene el par clave-valor para el argumento de línea de comandos de
configuración proporcionado a dotnet run .
Argumentos
El valor debe seguir a un signo igual ( = ) o la clave debe tener un prefijo ( -- o / ) cuando el valor
siga a un espacio. El valor puede ser NULL si se usa un signo igual (por ejemplo, CommandLineKey= ).

PREFIJO DE LA CLAVE EJEMPLO

Sin prefijo CommandLineKey1=value1

Dos guiones ( -- ) --CommandLineKey2=value2 ,


--CommandLineKey2 value2

Barra diagonal ( / ) /CommandLineKey3=value3 ,


/CommandLineKey3 value3

Dentro del mismo comando, no mezcle pares clave-valor de argumento de la línea de comandos que
usan un signo igual con pares de clave-valor que usan un espacio.
Comandos de ejemplo:

dotnet run CommandLineKey1=value --CommandLineKey2=value /CommandLineKey2=value


dotnet run --CommandLineKey1 value /CommandLineKey2 value
dotnet run CommandLineKey1= CommandLineKey2=value

Asignaciones de modificador
Las asignaciones de modificador admiten la lógica de sustitución de nombres de clave. Cuando crea
manualmente la configuración con ConfigurationBuilder, puede proporcionar un diccionario de
reemplazos de modificador al método AddCommandLine.
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 el par clave-valor en la configuración de la aplicació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.

public class Program


{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}

public static IWebHostBuilder CreateWebHostBuilder(string[] args)


{
var switchMappings = new Dictionary<string, string>
{
{ "-CLKey1", "CommandLineKey1" },
{ "-CLKey2", "CommandLineKey2" }
};

var config = new ConfigurationBuilder()


.AddCommandLine(args, switchMappings)
.Build();

return WebHost.CreateDefaultBuilder()
.UseConfiguration(config)
.UseStartup<Startup>();
}
}

Como se muestra en el ejemplo anterior, la llamada a CreateDefaultBuilder no debería pasar


argumentos cuando se usan las asignaciones de modificador. La llamada de AddCommandLine del
método CreateDefaultBuilder no incluye modificadores asignados y no hay forma de pasar el
diccionario de asignación de modificador a CreateDefaultBuilder . Si los argumentos incluyen un
conmutador asignado y se pasan a CreateDefaultBuilder , el proveedor de AddCommandLine no se
puede inicializar con una FormatException. La solución no es pasar los argumentos de
CreateDefaultBuilder , sino que permitir que el método AddCommandLine del método
ConfigurationBuilder procese tanto los argumentos como el diccionario de asignación de
modificador.
public static void Main(string[] args)
{
var switchMappings = new Dictionary<string, string>
{
{ "-CLKey1", "CommandLineKey1" },
{ "-CLKey2", "CommandLineKey2" }
};

var config = new ConfigurationBuilder()


.AddCommandLine(args, switchMappings)
.Build();

var host = new WebHostBuilder()


.UseConfiguration(config)
.UseKestrel()
.UseStartup<Startup>()
.Start();

using (host)
{
Console.ReadLine();
}
}

Después de crear el diccionario de asignaciones de modificador, contiene los datos que se muestran en
la tabla siguiente.

KEY VALOR

-CLKey1 CommandLineKey1

-CLKey2 CommandLineKey2

Si las claves de asignación de conmutador se usan al iniciar la aplicación, la configuración recibe el


valor de configuración en la clave que el diccionario suministra:

dotnet run -CLKey1=value1 -CLKey2=value2

Una vez que se ejecuta el comando anterior, la configuración contiene los valores que se muestran en
la tabla siguiente.

KEY VALOR

CommandLineKey1 value1

CommandLineKey2 value2

Proveedor de configuración de variables de entorno


EnvironmentVariablesConfigurationProvider carga la configuración de pares clave-valor de variables
de entorno en tiempo de ejecución.
Para activar la configuración de variables de entorno, llame al método de extensión
AddEnvironmentVariables en una instancia de ConfigurationBuilder.
Al trabajar con claves jerárquicas en variables de entorno, es posible que un separador de dos puntos (
: ) no funcione en todas las plataformas. Un guion bajo doble ( __ ) es compatible con todas las
plataformas y lo reemplaza un separador de dos puntos.
Azure App Service permite establecer las variables de entorno en Azure Portal que pueden invalidar la
configuración de la aplicación mediante el proveedor de configuración de variables de entorno. Para
más información, consulte el artículo sobre Azure Apps: Invalidación de la configuración de la
aplicación con Azure Portal.
AddEnvironmentVariables se llama automáticamente cuando se inicializa un nuevo WebHostBuilder
con CreateDefaultBuilder. Para más información, consulte Host web: Configuración de un host.
CreateDefaultBuilder también carga:
Configuración opcional de appsettings.json y appsettings.< Environment>.json.
Secretos de usuario (Administrador de secretos) (en el entorno de desarrollo).
Argumentos de la línea de comandos.
El proveedor de configuración de variables de entorno se llama una vez establecida la configuración
desde los secretos de usuario y los archivos appsettings. Llamar al proveedor en esta posición permite
que la lectura de las variables de entorno en tiempo de ejecución invaliden la configuración
establecida por los secretos de usuario y los archivos appsettings.
Puede llamar directamente al método de extensión AddEnvironmentVariables en una instancia de
ConfigurationBuilder:
Aplique la configuración a WebHostBuilder con el método UseConfiguration :

var config = new ConfigurationBuilder()


.AddEnvironmentVariables()
.Build();

var host = new WebHostBuilder()


.UseConfiguration(config)
.UseKestrel()
.UseStartup<Startup>();

Ejemplo
La aplicación de ejemplo 2.x aprovecha el método de conveniencia estático CreateDefaultBuilder para
crear el host, que incluye una llamada a AddEnvironmentVariables .
La aplicación de ejemplo 1.x llama a AddEnvironmentVariables en ConfigurationBuilder .
1. Ejecute la aplicación de ejemplo. Abra un explorador a la aplicación en http://localhost:5000 .
2. Observe que el resultado contiene el par clave y valor para la variable de entorno ENVIRONMENT . El
valor refleja el entorno en el que se ejecuta la aplicación, habitualmente Development cuando se
ejecuta de manera local.
Para que la lista de variables de entorno que muestra la aplicación sea breve, la aplicación filtra las
variables de entorno que comienzan con lo siguiente:
ASPNETCORE_
urls
Registro
ENVIRONMENT
contentRoot
AllowedHosts
applicationName
CommandLine
Si quiere exponer todas las variables de entorno disponibles para la aplicación, cambie
FilteredConfiguration en Pages/Index.cshtml.cs a lo siguiente:

Si quiere exponer todas las variables de entorno disponibles para la aplicación, cambie
FilteredConfiguration en Controllers/HomeController.cs a lo siguiente:

FilteredConfiguration = _config.AsEnumerable();

Prefijos
Las variables de entorno que se cargan en la configuración de la aplicación se filtran cuando se
proporciona un prefijo para el método AddEnvironmentVariables . Por ejemplo, para filtrar las variables
de entorno en el prefijo CUSTOM_ , suministre el prefijo para el proveedor de configuración:

var config = new ConfigurationBuilder()


.AddEnvironmentVariables("CUSTOM_")
.Build();

El prefijo se quita cuando se crean los pares clave-valor de configuración.


El método de conveniencia estático CreateDefaultBuilder crea un WebHostBuilder para establecer el
host de la aplicación. Cuando se crea WebHostBuilder , encuentra su configuración de host en variables
de entorno con el prefijo ASPNETCORE_ .
Prefijos de cadena de conexión
La API de configuración tiene reglas de procesamiento especial para cuatro variables de entorno de
cadena de conexión relacionadas con la configuración de las cadenas de conexión de Azure para el
entorno de la aplicación. Las variables de entorno con los prefijos que se muestran en la tabla se
cargan en la aplicación si no se proporciona ningún prefijo a AddEnvironmentVariables .

PREFIJO DE CADENA DE CONEXIÓN PROVEEDOR

CUSTOMCONNSTR_ Proveedor personalizado

MYSQLCONNSTR_ MySQL

SQLAZURECONNSTR_ Azure SQL Database

SQLCONNSTR_ SQL Server

Cuando una variable de entorno se detecta y carga en la configuración con cualquiera de los cuatro
prefijos que se muestran en la tabla:
La clave de configuración se crea al quitar el prefijo de la variable de entorno y al agregar una
sección de clave de configuración ( ConnectionStrings ).
Se crea un nuevo par clave-valor de configuración que representa el proveedor de conexión de
base de datos (excepto para CUSTOMCONNSTR_ , que no tiene ningún proveedor indicado).
CLAVE DE CONFIGURACIÓN ENTRADA DE CONFIGURACIÓN DEL
CLAVE DE VARIABLE DE ENTORNO CONVERTIDA PROVEEDOR

CUSTOMCONNSTR_<KEY> ConnectionStrings:<KEY> Entrada de configuración no


creada.

MYSQLCONNSTR_<KEY> ConnectionStrings:<KEY> Clave:


ConnectionStrings:
<KEY>_ProviderName
:
Valor: MySql.Data.MySqlClient

SQLAZURECONNSTR_<KEY> ConnectionStrings:<KEY> Clave:


ConnectionStrings:
<KEY>_ProviderName
:
Valor: System.Data.SqlClient

SQLCONNSTR_<KEY> ConnectionStrings:<KEY> Clave:


ConnectionStrings:
<KEY>_ProviderName
:
Valor: System.Data.SqlClient

Proveedor de configuración de archivo


FileConfigurationProvider es la clase base para cargar la configuración del sistema de archivos. Los
proveedores de configuración siguientes están dedicados a determinados tipos de archivo:
Proveedor de configuración INI
Proveedor de configuración JSON
Proveedor de configuración XML
Proveedor de configuración de INI
IniConfigurationProvider carga la configuración desde pares clave-valor de archivo INI en tiempo de
ejecución.
Para activar la configuración de archivo INI, llame al método de extensión AddIniFile en una instancia
de ConfigurationBuilder.
Los dos puntos se pueden usar como un delimitador de sección en la configuración de archivo INI.
Las sobrecargas permiten especificar:
Si el archivo es opcional.
Si la configuración se recarga si el archivo cambia.
IFileProvider que se usa para acceder al archivo.
Al llamar a CreateDefaultBuilder , llame a UseConfiguration con la configuración:
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}

public static IWebHostBuilder CreateWebHostBuilder(string[] args)


{
var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddIniFile("config.ini", optional: true, reloadOnChange: true)
.Build();

return WebHost.CreateDefaultBuilder(args)
.UseConfiguration(config)
.UseStartup<Startup>();
}
}

Al crear directamente un WebHostBuilder, llame a UseConfiguration con la configuración:


Aplique la configuración a WebHostBuilder con el método UseConfiguration :

var config = new ConfigurationBuilder()


.SetBasePath(Directory.GetCurrentDirectory())
.AddIniFile("config.ini", optional: true, reloadOnChange: true)
.Build();

var host = new WebHostBuilder()


.UseConfiguration(config)
.UseKestrel()
.UseStartup<Startup>();

Un ejemplo genérico de un archivo de configuración INI:

[section0]
key0=value
key1=value

[section1]
subsection:key=value

[section2:subsection0]
key=value

[section2:subsection1]
key=value

El archivo de configuración anterior carga las siguientes claves con value :


section0:key0
section0:key1
section1:subsection:key
section2:subsection0:key
section2:subsection1:key
Proveedor de configuración JSON
JsonConfigurationProvider carga la configuración desde pares clave-valor de archivos JSON en
tiempo de ejecución.
Para activar la configuración de archivo JSON, llame al método de extensión AddJsonFile en una
instancia de ConfigurationBuilder.
Las sobrecargas permiten especificar:
Si el archivo es opcional.
Si la configuración se recarga si el archivo cambia.
IFileProvider que se usa para acceder al archivo.
AddJsonFile se llama automáticamente dos veces al inicializar un nuevo WebHostBuilder con
CreateDefaultBuilder. Se llama al método para cargar la configuración desde:
appsettings.json – Este archivo se lee primero. La versión del entorno del archivo puede invalidar
los valores que proporciona el archivo appsettings.json.
appsettings.<Environment>.json – La versión del entorno del archivo se carga en función de
IHostingEnvironment.EnvironmentName.
Para más información, consulte Host web: Configuración de un host.
CreateDefaultBuilder también carga:
Variables de entorno.
Secretos de usuario (Administrador de secretos) (en el entorno de desarrollo).
Argumentos de la línea de comandos.
El proveedor de configuración de JSON se establece en primer lugar. Por tanto, los secretos de
usuario, las variables de entorno y los argumentos de la línea de comandos invalidan la configuración
establecida por los archivos appsettings.
Puede llamar directamente al método de extensión AddJsonFile en una instancia de
ConfigurationBuilder.
Al llamar a CreateDefaultBuilder , llame a UseConfiguration con la configuración:

public class Program


{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}

public static IWebHostBuilder CreateWebHostBuilder(string[] args)


{
var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("config.json", optional: true, reloadOnChange: true)
.Build();

return WebHost.CreateDefaultBuilder(args)
.UseConfiguration(config)
.UseStartup<Startup>();
}
}

Al crear directamente un WebHostBuilder, llame a UseConfiguration con la configuración:


Aplique la configuración a WebHostBuilder con el método UseConfiguration :
var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("config.json", optional: true, reloadOnChange: true)
.Build();

var host = new WebHostBuilder()


.UseConfiguration(config)
.UseKestrel()
.UseStartup<Startup>();

Ejemplo
La aplicación de ejemplo 2.x aprovecha el método de conveniencia estático CreateDefaultBuilder para
crear el host, que incluye dos llamadas a AddJsonFile . La configuración se carga desde
appsettings.json y appsettings.<Environment>.json.
La aplicación de ejemplo 1.x llama a AddJsonFile dos veces en un ConfigurationBuilder . La
configuración se carga desde appsettings.json y appsettings.<Environment>.json.
1. Ejecute la aplicación de ejemplo. Abra un explorador a la aplicación en http://localhost:5000 .
2. Observe que la salida contiene los pares clave-valor para la configuración que se muestra en la
tabla según el entorno. Las claves de configuración de registro usan dos puntos ( : ) como
separador jerárquico.

KEY VALOR DE DESARROLLO VALOR DE PRODUCCIÓN

Logging:LogLevel:System Información Información

Logging:LogLevel:Microsoft Información Información

Logging:LogLevel:Default Depuración Error

AllowedHosts * *

Proveedor de configuración XML


XmlConfigurationProvider carga la configuración desde pares clave-valor de archivo XML en tiempo
de ejecución.
Para activar la configuración de archivo XML, llame al método de extensión AddXmlFile en una
instancia de ConfigurationBuilder.
Las sobrecargas permiten especificar:
Si el archivo es opcional.
Si la configuración se recarga si el archivo cambia.
IFileProvider que se usa para acceder al archivo.
El nodo raíz del archivo de configuración se omite cuando se crean los pares clave-valor de
configuración. No especifique una definición de tipo de documento (DTD ) ni el espacio de nombres en
el archivo.
Al llamar a CreateDefaultBuilder , llame a UseConfiguration con la configuración:
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}

public static IWebHostBuilder CreateWebHostBuilder(string[] args)


{
var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddXmlFile("config.xml", optional: true, reloadOnChange: true)
.Build();

return WebHost.CreateDefaultBuilder(args)
.UseConfiguration(config)
.UseStartup<Startup>();
}
}

Al crear directamente un WebHostBuilder, llame a UseConfiguration con la configuración:


Aplique la configuración a WebHostBuilder con el método UseConfiguration :

var config = new ConfigurationBuilder()


.SetBasePath(Directory.GetCurrentDirectory())
.AddXmlFile("config.xml", optional: true, reloadOnChange: true)
.Build();

var host = new WebHostBuilder()


.UseConfiguration(config)
.UseKestrel()
.UseStartup<Startup>();

Los archivos de configuración XML pueden usar distintos nombres de elemento para las secciones
repetidas:

<?xml version="1.0" encoding="UTF-8"?>


<configuration>
<section0>
<key0>value</key0>
<key1>value</key1>
</section0>
<section1>
<key0>value</key0>
<key1>value</key1>
</section1>
</configuration>

El archivo de configuración anterior carga las siguientes claves con value :


section0:key0
section0:key1
section1:key0
section1:key1
Los elementos de repetición que usan el mismo nombre de elemento funcionan si el atributo name se
usa para distinguir los elementos:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<section name="section0">
<key name="key0">value</key>
<key name="key1">value</key>
</section>
<section name="section1">
<key name="key0">value</key>
<key name="key1">value</key>
</section>
</configuration>

El archivo de configuración anterior carga las siguientes claves con value :


section:section0:key:key0
section:section0:key:key1
section:section1:key:key0
section:section1:key:key1
Los atributos se pueden usar para suministrar valores:

<?xml version="1.0" encoding="UTF-8"?>


<configuration>
<key attribute="value" />
<section>
<key attribute="value" />
</section>
</configuration>

El archivo de configuración anterior carga las siguientes claves con value :


key:attribute
section:key:attribute

Proveedor de configuración de clave por archivo


KeyPerFileConfigurationProvider usa los archivos de un directorio como pares clave-valor de
configuración. La clave es el nombre de archivo. El valor contiene el contenido del archivo. El
proveedor de configuración de clave por archivo se usa en escenarios de hospedaje de Docker.
Para activar la configuración de clave por archivo, llame al método de extensión AddKeyPerFile en una
instancia de ConfigurationBuilder. La ruta de acceso directoryPath a los archivos debe ser una ruta
de acceso absoluta.
Las sobrecargas permiten especificar:
Un delegado Action<KeyPerFileConfigurationSource> que configura el origen.
Si el directorio es opcional y la ruta de acceso al directorio.
Al llamar a CreateDefaultBuilder , llame a UseConfiguration con la configuración:
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}

public static IWebHostBuilder CreateWebHostBuilder(string[] args)


{
var path = Path.Combine(Directory.GetCurrentDirectory(), "path/to/files");
var config = new ConfigurationBuilder()
.AddKeyPerFile(directoryPath: path, optional: true)
.Build();

return WebHost.CreateDefaultBuilder(args)
.UseConfiguration(config)
.UseStartup<Startup>();
}
}

Al crear directamente un WebHostBuilder, llame a UseConfiguration con la configuración:

var path = Path.Combine(Directory.GetCurrentDirectory(), "path/to/files");


var config = new ConfigurationBuilder()
.AddKeyPerFile(directoryPath: path, optional: true)
.Build();

var host = new WebHostBuilder()


.UseConfiguration(config)
.UseKestrel()
.UseStartup<Startup>();

Proveedor de configuración de memoria


MemoryConfigurationProvider usa una colección en memoria como pares clave-valor de
configuración.
Para activar la configuración de colección en memoria, llame al método de extensión
AddInMemoryCollection en una instancia de ConfigurationBuilder.
El proveedor de configuración se puede inicializar con un IEnumerable<KeyValuePair<String,String>> .
Al llamar a CreateDefaultBuilder , llame a UseConfiguration con la configuración:
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}

public static IWebHostBuilder CreateWebHostBuilder(string[] args)


{
var dict = new Dictionary<string, string>
{
{"MemoryCollectionKey1", "value1"},
{"MemoryCollectionKey2", "value2"}
};

var config = new ConfigurationBuilder()


.AddInMemoryCollection(dict)
.Build();

return WebHost.CreateDefaultBuilder(args)
.UseConfiguration(config)
.UseStartup<Startup>();
}
}

Al crear directamente un WebHostBuilder, llame a UseConfiguration con la configuración:


Aplique la configuración a WebHostBuilder con el método UseConfiguration :

var dict = new Dictionary<string, string>


{
{"MemoryCollectionKey1", "value1"},
{"MemoryCollectionKey2", "value2"}
};

var config = new ConfigurationBuilder()


.AddInMemoryCollection(dict)
.Build();

var host = new WebHostBuilder()


.UseConfiguration(config)
.UseKestrel()
.UseStartup<Startup>();

GetValue
ConfigurationBinder.GetValue<T> extrae un valor de la configuración con una clave especificada y lo
convierte al tipo especificado. Una sobrecarga permite proporcionar un valor predeterminado si no se
encuentra la clave.
El ejemplo siguiente extrae el valor de cadena desde la configuración con la clave NumberKey , escribe
el valor como int y almacena el valor en la variable intValue . Si NumberKey no se encuentra en las
claves de configuración, intValue recibe el valor predeterminado de 99 :

var intValue = config.GetValue<int>("NumberKey", 99);

GetSection, GetChildren y Exists


Para los ejemplos siguientes, considere el siguiente archivo JSON. Se encuentran cuatro claves en dos
secciones, una de las cuales incluye un par de subsecciones:

{
"section0": {
"key0": "value",
"key1": "value"
},
"section1": {
"key0": "value",
"key1": "value"
},
"section2": {
"subsection0" : {
"key0": "value",
"key1": "value"
},
"subsection1" : {
"key0": "value",
"key1": "value"
}
}
}

Cuando el archivo se lee en la configuración, se crean las siguientes claves jerárquicas únicas para
contener los valores de configuración:
section0:key0
section0:key1
section1:key0
section1:key1
section2:subsection0:key0
section2:subsection0:key1
section2:subsection1:key0
section2:subsection1:key1
GetSection
IConfiguration.GetSection extrae una subsección de la configuración con la clave de subsección
especificada.
Para devolver un IConfigurationSection que contiene los pares clave-valor en section1 , llame a
GetSection y suministre el nombre de la sección:

var configSection = _config.GetSection("section1");

De forma similar, para obtener los valores de las claves de section2:subsection0 , llame a GetSection
y suministre la ruta de acceso a la sección:

var configSection = _config.GetSection("section2:subsection0");

GetSection nunca devuelve null . Si no se encuentra una sección que coincida, se devuelve una
IConfigurationSection vacía.

GetChildren
Una llamada a IConfiguration.GetChildren en section2 obtiene una
IEnumerable<IConfigurationSection> que incluye:
subsection0
subsection1

var configSection = _config.GetSection("section2");

var children = configSection.GetChildren();

Existe
Use ConfigurationExtensions.Exists para determinar si existe una sección de configuración:

var sectionExists = _config.GetSection("section2:subsection2").Exists();

Dados los datos de ejemplo, sectionExists es false porque no hay una sección
section2:subsection2 en los datos de configuración.

Enlace a una clase


La configuración se puede enlazar a clases que representan grupos de configuraciones relacionadas a
través del patrón de opciones. Para obtener más información, vea Patrón de opciones en ASP.NET
Core.
Los valores de configuración se devuelven como cadenas, pero llamar a Bind permite la construcción
de objetos POCO.
La aplicación de ejemplo contiene un modelo Starship (Models/Starship.cs):

public class Starship


{
public string Name { get; set; }
public string Registry { get; set; }
public string Class { get; set; }
public decimal Length { get; set; }
public bool Commissioned { get; set; }
}

public class Starship


{
public string Name { get; set; }
public string Registry { get; set; }
public string Class { get; set; }
public decimal Length { get; set; }
public bool Commissioned { get; set; }
}

La sección starship del archivo starship.json crea la configuración cuando la aplicación de ejemplo
usa el proveedor de configuración JSON para cargar la configuración:
{
"starship": {
"name": "USS Enterprise",
"registry": "NCC-1701",
"class": "Constitution",
"length": 304.8,
"commissioned": false
},
"trademark": "Paramount Pictures Corp. http://www.paramount.com"
}

{
"starship": {
"name": "USS Enterprise",
"registry": "NCC-1701",
"class": "Constitution",
"length": 304.8,
"commissioned": false
},
"trademark": "Paramount Pictures Corp. http://www.paramount.com"
}

Se crean los siguientes pares clave-valor de configuración:

KEY VALOR

starship:name USS Enterprise

starship:registry NCC-1701

starship:class Constitution

starship:length 304.8

starship:commissioned False

trademark Paramount Pictures Corp. http://www.paramount.com

La aplicación de ejemplo llama a GetSection con la clave starship . Los pares clave-valor starship
están aislados. El método Bind se llama en la subsección que pasa una instancia de la clase Starship .
Después de enlazar los valores de instancia, la instancia está asignada a una propiedad para la
representación:

var starship = new Starship();


_config.GetSection("starship").Bind(starship);
Starship = starship;

var starship = new Starship();


_config.GetSection("starship").Bind(starship);
viewModel.Starship = starship;

Enlazar a un gráfico de objetos


Bind es capaz de enlazar todo un gráfico de objetos POCO.
El ejemplo contiene un modelo TvShow cuyo gráfico de objetos incluye las clases Metadata y Actors
(Models/TvShow.cs):

public class TvShow


{
public Metadata Metadata { get; set; }
public Actors Actors { get; set; }
public string Legal { get; set; }
}

public class Metadata


{
public string Series { get; set; }
public string Title { get; set; }
public DateTime AirDate { get; set; }
public int Episodes { get; set; }
}

public class Actors


{
public string Names { get; set; }
}

public class TvShow


{
public Metadata Metadata { get; set; }
public Actors Actors { get; set; }
public string Legal { get; set; }
}

public class Metadata


{
public string Series { get; set; }
public string Title { get; set; }
public DateTime AirDate { get; set; }
public int Episodes { get; set; }
}

public class Actors


{
public string Names { get; set; }
}

La aplicación de ejemplo tiene un archivo tvshow.xml que contiene los datos de configuración:

<?xml version="1.0" encoding="UTF-8"?>


<configuration>
<tvshow>
<metadata>
<series>Dr. Who</series>
<title>The Sun Makers</title>
<airdate>11/26/1977</airdate>
<episodes>4</episodes>
</metadata>
<actors>
<names>Tom Baker, Louise Jameson, John Leeson</names>
</actors>
<legal>(c)1977 BBC https://www.bbc.co.uk/programmes/b006q2x0</legal>
</tvshow>
</configuration>
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<tvshow>
<metadata>
<series>Dr. Who</series>
<title>The Sun Makers</title>
<airdate>11/26/1977</airdate>
<episodes>4</episodes>
</metadata>
<actors>
<names>Tom Baker, Louise Jameson, John Leeson</names>
</actors>
<legal>(c)1977 BBC https://www.bbc.co.uk/programmes/b006q2x0</legal>
</tvshow>
</configuration>

Configuración está enlazada a todo el gráfico de objetos TvShow con el método Bind . La instancia
enlazada se asigna a una propiedad para la representación:

var tvShow = new TvShow();


_config.GetSection("tvshow").Bind(tvShow);
TvShow = tvShow;

var tvShow = new TvShow();


_config.GetSection("tvshow").Bind(tvShow);
viewModel.TvShow = tvShow;

ConfigurationBinder.Get<T> enlaza y devuelve el tipo especificado. Get<T> es más conveniente que


usar Bind . El código siguiente muestra cómo usar Get<T> con el ejemplo anterior, lo que permite que
la instancia enlazada se asigne directamente a la propiedad que se usa para la representación:

TvShow = _config.GetSection("tvshow").Get<TvShow>();

viewModel.TvShow = _config.GetSection("tvshow").Get<TvShow>();

Enlace de una matriz a una clase


La aplicación de ejemplo muestra los conceptos que se explican en esta sección.
Bind admite enlazar matrices a objetos con los índices de matriz en las claves de configuración.
Cualquier formato de matriz que expone un segmento de clave numérica ( :0: , :1: , … :{n}: ) es
capaz de enlazar una matriz a una matriz de clase POCO.

NOTE
El enlace se proporciona por convención. Los proveedores de configuración personalizados no son necesarios
para implementar el enlace de matriz.

Procesamiento de matriz en memoria


Considere los valores y las claves de configuración que se muestran en esta tabla.
KEY VALOR

array:0 value0

array:1 value1

array:2 value2

array:4 value4

array:5 value5

Estas claves y valores se cargan en la aplicación de ejemplo a través del proveedor de configuración de
memoria:

public class Program


{
public static Dictionary<string, string> arrayDict = new Dictionary<string, string>
{
{"array:entries:0", "value0"},
{"array:entries:1", "value1"},
{"array:entries:2", "value2"},
{"array:entries:4", "value4"},
{"array:entries:5", "value5"}
};

public static void Main(string[] args)


{
CreateWebHostBuilder(args).Build().Run();
}

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>


WebHost.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((hostingContext, config) =>
{
config.SetBasePath(Directory.GetCurrentDirectory());
config.AddInMemoryCollection(arrayDict);
config.AddJsonFile("json_array.json", optional: false, reloadOnChange: false);
config.AddJsonFile("starship.json", optional: false, reloadOnChange: false);
config.AddXmlFile("tvshow.xml", optional: false, reloadOnChange: false);
config.AddEFConfiguration(options => options.UseInMemoryDatabase("InMemoryDb"));
config.AddCommandLine(args);
})
.UseStartup<Startup>();
}
public class Startup
{
public Startup(IHostingEnvironment env)
{
var arrayDict = new Dictionary<string, string>
{
{"array:entries:0", "value0"},
{"array:entries:1", "value1"},
{"array:entries:2", "value2"},
{"array:entries:4", "value4"},
{"array:entries:5", "value5"}
};

var builder = new ConfigurationBuilder()


.SetBasePath(Directory.GetCurrentDirectory())
.AddInMemoryCollection(arrayDict)
.AddJsonFile("json_array.json", optional: false, reloadOnChange: false)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true,
reloadOnChange: true)
.AddEnvironmentVariables()
.AddJsonFile("starship.json", optional: false, reloadOnChange: false)
.AddXmlFile("tvshow.xml", optional: false, reloadOnChange: false)
.AddEFConfiguration(options => options.UseInMemoryDatabase("InMemoryDb"))
.AddCommandLine(Program.Args);

Configuration = builder.Build();
}

La matriz omite un valor de índice #3. El enlazador de configuración no es capaz de enlazar los valores
NULL ni de crear entradas NULL en los objetos enlazados, lo que queda claro cuando se muestra el
resultado de enlazar esta matriz a un objeto.
En la aplicación de ejemplo, hay disponible una clase POCO para almacenar los datos de
configuración enlazados:

public class ArrayExample


{
public string[] Entries { get; set; }
}

public class ArrayExample


{
public string[] Entries { get; set; }
}

Los datos de configuración están enlazados al objeto:

var arrayExample = new ArrayExample();


_config.GetSection("array").Bind(arrayExample);

También se puede usar la sintaxis de ConfigurationBinder.Get<T> , lo que genera un código más


compacto:

ArrayExample = _config.GetSection("array").Get<ArrayExample>();
viewModel.ArrayExample = _config.GetSection("array").Get<ArrayExample>();

El objeto enlazado, una instancia de ArrayExample , recibe los datos de la matriz desde la configuración.

ÍNDICE DE ARRAYEXAMPLES.ENTRIES VALOR DE ARRAYEXAMPLES.ENTRIES

0 value0

1 value1

2 value2

3 value4

4 value5

El índice #3 en el objeto enlazado contiene los datos de configuración para la clave de configuración
array:4 y su valor de value4 . Cuando se enlazan datos de configuración que contienen una matriz,
los índices de la matriz en las claves de configuración solo se usan para iterar los datos de
configuración al crear el objeto. No se puede conservar un valor NULL en los datos de configuración y
no se crea una entrada con valores NULL en un objeto enlazado cuando una matriz en las claves de
configuración omite uno o más índices.
El elemento de configuración omitido para el índice #3 se puede proporcionar antes del enlace a la
instancia ArrayExamples por cualquier proveedor de configuración que genera el par clave-valor
correcto en la configuración. Si el ejemplo incluía un proveedor de configuración JSON adicional con
el par clave-valor omitido, ArrayExamples.Entries coincide con la matriz de configuración completa:
missing_value.json:

{
"array:entries:3": "value3"
}

En ConfigureAppConfiguration :

config.AddJsonFile("missing_value.json", optional: false, reloadOnChange: false);

En el constructor Startup :

.AddJsonFile("missing_value.json", optional: false, reloadOnChange: false);

El par clave-valor que se muestra en la tabla se carga en la configuración.

KEY VALOR

array:entries:3 value3

Si la instancia de clase ArrayExamples se enlaza después de que el proveedor de configuración JSON


incluye la entrada para el índice #3, la matriz ArrayExamples.Entries incluye el valor.
ÍNDICE DE ARRAYEXAMPLES.ENTRIES VALOR DE ARRAYEXAMPLES.ENTRIES

0 value0

1 value1

2 value2

3 value3

4 value4

5 value5

Procesamiento de matriz JSON


Si un archivo JSON contiene una matriz, se crean claves de configuración para los elementos de la
matriz con un índice de sección basado en cero. En el siguiente archivo de configuración, subsection
es una matriz:

{
"json_array": {
"key": "valueA",
"subsection": [
"valueB",
"valueC",
"valueD"
]
}
}

{
"json_array": {
"key": "valueA",
"subsection": [
"valueB",
"valueC",
"valueD"
]
}
}

El proveedor de configuración JSON lee los datos de configuración en los siguientes pares clave-
valor:

KEY VALOR

json_array:key valueA

json_array:subsection:0 valueB

json_array:subsection:1 valueC

json_array:subsection:2 valueD
En la aplicación de ejemplo, la clase POCO siguiente está disponible para enlazar los pares clave-valor
de configuración:

public class JsonArrayExample


{
public string Key { get; set; }
public string[] Subsection { get; set; }
}

public class JsonArrayExample


{
public string Key { get; set; }
public string[] Subsection { get; set; }
}

Después del enlace, JsonArrayExample.Key contiene el valor valueA . Los valores de la subsección se
almacenan en la propiedad de la matriz POCO, Subsection .

ÍNDICE DE JSONARRAYEXAMPLE.SUBSECTION VALOR DE JSONARRAYEXAMPLE.SUBSECTION

0 valueB

1 valueC

2 valueD

Proveedores de configuración personalizada


La aplicación de ejemplo muestra cómo crear un proveedor de configuración básica que lee los pares
clave-valor de configuración desde una base de datos mediante Entity Framework (EF ).
El proveedor tiene las siguientes características:
La base de datos en memoria de EF se usa para fines de demostración. Para usar una base de datos
que requiere una cadena de conexión, implemente un ConfigurationBuilder secundario para
suministrar la cadena de conexión desde otro proveedor de configuración.
El proveedor lee una tabla de base de datos en la configuración en el inicio. El proveedor no
consulta la base de datos por clave.
La función de recarga en cambio no se implementa, por lo que actualizar la base de datos después
del inicio de la aplicación no afecta a la configuración de la aplicación.
Defina una entidad EFConfigurationValue para almacenar los valores de configuración en la base de
datos.
Models/EFConfigurationValue.cs:

public class EFConfigurationValue


{
public string Id { get; set; }
public string Value { get; set; }
}
public class EFConfigurationValue
{
public string Id { get; set; }
public string Value { get; set; }
}

Agregue un EFConfigurationContext para almacenar y tener acceso a los valores configurados.


EFConfigurationProvider/EFConfigurationContext.cs:

public class EFConfigurationContext : DbContext


{
public EFConfigurationContext(DbContextOptions options) : base(options)
{
}

public DbSet<EFConfigurationValue> Values { get; set; }


}

public class EFConfigurationContext : DbContext


{
public EFConfigurationContext(DbContextOptions options) : base(options)
{
}

public DbSet<EFConfigurationValue> Values { get; set; }


}

Cree una clase que implemente IConfigurationSource.


EFConfigurationProvider/EFConfigurationSource.cs:

public class EFConfigurationSource : IConfigurationSource


{
private readonly Action<DbContextOptionsBuilder> _optionsAction;

public EFConfigurationSource(Action<DbContextOptionsBuilder> optionsAction)


{
_optionsAction = optionsAction;
}

public IConfigurationProvider Build(IConfigurationBuilder builder)


{
return new EFConfigurationProvider(_optionsAction);
}
}
public class EFConfigurationSource : IConfigurationSource
{
private readonly Action<DbContextOptionsBuilder> _optionsAction;

public EFConfigurationSource(Action<DbContextOptionsBuilder> optionsAction)


{
_optionsAction = optionsAction;
}

public IConfigurationProvider Build(IConfigurationBuilder builder)


{
return new EFConfigurationProvider(_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.
EFConfigurationProvider/EFConfigurationProvider.cs:
public class EFConfigurationProvider : ConfigurationProvider
{
public EFConfigurationProvider(Action<DbContextOptionsBuilder> optionsAction)
{
OptionsAction = optionsAction;
}

Action<DbContextOptionsBuilder> OptionsAction { get; }

// Load config data from EF DB.


public override void Load()
{
var builder = new DbContextOptionsBuilder<EFConfigurationContext>();

OptionsAction(builder);

using (var dbContext = new EFConfigurationContext(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(


EFConfigurationContext dbContext)
{
// Quotes (c)2005 Universal Pictures: Serenity
// https://www.uphe.com/movies/serenity
var configValues = new Dictionary<string, string>
{
{ "quote1", "I aim to misbehave." },
{ "quote2", "I swallowed a bug." },
{ "quote3", "You can't stop the signal, Mal." }
};

dbContext.Values.AddRange(configValues
.Select(kvp => new EFConfigurationValue
{
Id = kvp.Key,
Value = kvp.Value
})
.ToArray());

dbContext.SaveChanges();

return configValues;
}
}
public class EFConfigurationProvider : ConfigurationProvider
{
public EFConfigurationProvider(Action<DbContextOptionsBuilder> optionsAction)
{
OptionsAction = optionsAction;
}

Action<DbContextOptionsBuilder> OptionsAction { get; }

// Load config data from EF DB.


public override void Load()
{
var builder = new DbContextOptionsBuilder<EFConfigurationContext>();

OptionsAction(builder);

using (var dbContext = new EFConfigurationContext(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(


EFConfigurationContext dbContext)
{
// Quotes (c)2005 Universal Pictures: Serenity
// https://www.uphe.com/movies/serenity
var configValues = new Dictionary<string, string>
{
{ "quote1", "I aim to misbehave." },
{ "quote2", "I swallowed a bug." },
{ "quote3", "You can't stop the signal, Mal." }
};

dbContext.Values.AddRange(configValues
.Select(kvp => new EFConfigurationValue
{
Id = kvp.Key,
Value = kvp.Value
})
.ToArray());

dbContext.SaveChanges();

return configValues;
}
}

Un método de extensión AddEFConfiguration permite agregar el origen de configuración a un


ConfigurationBuilder .

Extensions/EntityFrameworkExtensions.cs:
public static class EntityFrameworkExtensions
{
public static IConfigurationBuilder AddEFConfiguration(
this IConfigurationBuilder builder,
Action<DbContextOptionsBuilder> optionsAction)
{
return builder.Add(new EFConfigurationSource(optionsAction));
}
}

public static class EntityFrameworkExtensions


{
public static IConfigurationBuilder AddEFConfiguration(
this IConfigurationBuilder builder,
Action<DbContextOptionsBuilder> optionsAction)
{
return builder.Add(new EFConfigurationSource(optionsAction));
}
}

En el código siguiente se muestra cómo puede usar el EFConfigurationProvider personalizado en


Program.cs:

public class Program


{
public static Dictionary<string, string> arrayDict = new Dictionary<string, string>
{
{"array:entries:0", "value0"},
{"array:entries:1", "value1"},
{"array:entries:2", "value2"},
{"array:entries:4", "value4"},
{"array:entries:5", "value5"}
};

public static void Main(string[] args)


{
CreateWebHostBuilder(args).Build().Run();
}

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>


WebHost.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((hostingContext, config) =>
{
config.SetBasePath(Directory.GetCurrentDirectory());
config.AddInMemoryCollection(arrayDict);
config.AddJsonFile("json_array.json", optional: false, reloadOnChange: false);
config.AddJsonFile("starship.json", optional: false, reloadOnChange: false);
config.AddXmlFile("tvshow.xml", optional: false, reloadOnChange: false);
config.AddEFConfiguration(options => options.UseInMemoryDatabase("InMemoryDb"));
config.AddCommandLine(args);
})
.UseStartup<Startup>();
}
public class Startup
{
public Startup(IHostingEnvironment env)
{
var arrayDict = new Dictionary<string, string>
{
{"array:entries:0", "value0"},
{"array:entries:1", "value1"},
{"array:entries:2", "value2"},
{"array:entries:4", "value4"},
{"array:entries:5", "value5"}
};

var builder = new ConfigurationBuilder()


.SetBasePath(Directory.GetCurrentDirectory())
.AddInMemoryCollection(arrayDict)
.AddJsonFile("json_array.json", optional: false, reloadOnChange: false)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true,
reloadOnChange: true)
.AddEnvironmentVariables()
.AddJsonFile("starship.json", optional: false, reloadOnChange: false)
.AddXmlFile("tvshow.xml", optional: false, reloadOnChange: false)
.AddEFConfiguration(options => options.UseInMemoryDatabase("InMemoryDb"))
.AddCommandLine(Program.Args);

Configuration = builder.Build();
}

Acceso a la configuración durante el inicio


Inserte en el constructor Startup para acceder a los valores de configuración en
IConfiguration
Startup.ConfigureServices . Para acceder a la configuración en Startup.Configure , inserte
IConfiguration directamente en el método o use la instancia desde el constructor:

public class Startup


{
private readonly IConfiguration _config;

public Startup(IConfiguration config)


{
_config = config;
}

public void ConfigureServices(IServiceCollection services)


{
var value = _config["key"];
}

public void Configure(IApplicationBuilder app, IConfiguration config)


{
var value = config["key"];
}
}

Para un ejemplo de cómo acceder a la configuración a través de métodos de conveniencia de inicio,


consulte Inicio de la aplicación: Métodos de conveniencia.

Acceso a la configuración en una página de Razor Pages o en


una vista de MVC.
Para obtener acceso a los valores de configuración en una página de Razor Pages 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 en 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 value for 'key': @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 value for 'key': @Configuration["key"]</p>
</body>
</html>

Agregar 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, vea
Mejorar una aplicación desde un ensamblado externo en ASP.NET Core con IHostingStartup.

Recursos adicionales
Patrón de opciones en ASP.NET Core
Deep Dive into Microsoft Configuration (Profundización en la configuración de Microsoft)
Patrón de opciones en ASP.NET Core
31/08/2018 • 19 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 escenario 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 ): los escenarios (clases) que dependen de valores de
configuración dependen ú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.

Requisitos previos
Haga referencia al metapaquete Microsoft.AspNetCore.App o agregue una referencia de paquete al paquete
Microsoft.Extensions.Options.ConfigurationExtensions.
Haga referencia al metapaquete Microsoft.AspNetCore.All o agregue una referencia de paquete al paquete
Microsoft.Extensions.Options.ConfigurationExtensions.
Agregue una referencia al paquete Microsoft.Extensions.Options.ConfigurationExtensions.

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


NOTE
Al usar una instancia de ConfigurationBuilder personalizada para cargar las opciones de configuración desde un archivo
de configuración, confirme que la ruta de acceso base esté configurada correctamente:

var configBuilder = new ConfigurationBuilder()


.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true);
var config = configBuilder.Build();

services.Configure<MyOptions>(config);

Al cargar las opciones de configuración desde el archivo de configuración a través de CreateDefaultBuilder, no es


necesario establecer de forma explícita la ruta de acceso base.

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 escenarios (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.App (ASP.NET Core 2.1 o posterior), 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 opciones
(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 opciones
(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.
IOptionsSnapshot admite volver a cargar opciones con la mínima sobrecarga de procesamiento.
Cuando se accede a las opciones y se las almacena en caché durante la vigencia de la solicitud, se calculan una
vez por solicitud.
IOptionsSnapshot es una instantánea de IOptionsMonitor<TOptions> y se actualiza automáticamente cada
vez que el monitor desencadena cambios basados en los cambios del origen de datos.
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.
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
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).

Opciones de validación
Las opciones de validación permiten validar las opciones cuando se configuran. Llame a Validate con un
método de validación que devuelve true si las opciones son válidas y false si no lo son:

// Registration
services.AddOptions<MyOptions>("optionalOptionsName")
.Configure(o => { }) // Configure the options
.Validate(o => YourValidationShouldReturnTrueIfValid(o),
"custom error");

// Consumption
var monitor = services.BuildServiceProvider()
.GetService<IOptionsMonitor<MyOptions>>();

try
{
var options = monitor.Get("optionalOptionsName");
}
catch (OptionsValidationException e)
{
// e.OptionsName returns "optionalOptionsName"
// e.OptionsType returns typeof(MyOptions)
// e.Failures returns a list of errors, which would contain
// "custom error"
}

El ejemplo anterior establece la instancia de opciones con nombre en optionalOptionsName . La instancia


predeterminada es Options.DefaultName .
La validación se ejecuta cuando se crea la instancia de opciones. La instancia de opciones pasa seguro la
validación la primera vez que se accede.

IMPORTANT
La validación de opciones no protege contra las modificaciones de opciones después de configurarse y validarse
inicialmente.

El método Validate acepta una expresión Func<TOptions, bool> . Para personalizar completamente la
validación, implemente IValidateOptions<TOptions> , que permite:
Validación de varios tipos de opciones:
class ValidateTwo : IValidateOptions<Option1>, IValidationOptions<Option2>
Validación que depende de otro tipo de opción:
public DependsOnAnotherOptionValidator(IOptions<AnotherOption> options)

IValidateOptions valida:
Una instancia de opciones con nombre específica.
Todas las opciones cuando name es null .

Devuelve ValidateOptionsResult de la implementación de la interfaz:

public interface IValidateOptions<TOptions> where TOptions : class


{
ValidateOptionsResult Validate(string name, TOptions options);
}

En una futura versión, se implementará una validación diligente (fracasar rápido en el inicio) y una validación
basada en la anotación de datos.

IPostConfigureOptions
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>(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> 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.
IOptionsMonitor usa IOptionsMonitorCache<TOptions> para almacenar instancias de TOptions en caché.
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


IOptionspuede usarse en Startup.Configure , ya que los servicios se compilan antes de que se ejecute el
método Configure .

public void Configure(IApplicationBuilder app, IOptions<MyOptions> optionsAccessor)


{
var option1 = optionsAccessor.Value.Option1;
}

IOptions no se debe usar Startup.ConfigureServices . Puede que exista un estado incoherente de opciones
debido al orden de los registros de servicio.

Recursos adicionales
Configuración en ASP.NET Core
Registro en ASP.NET Core
21/09/2018 • 46 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.
Para obtener información sobre el registro de stdout al hospedar con IIS, consulte Solución de problemas de
ASP.NET Core en IIS. Para obtener información sobre el registro de stdout con Azure App Service, consulte
Solución de problemas de ASP.NET Core en Azure App Service.
Vea o descargue el código de ejemplo (cómo descargarlo)

Cómo crear registros


Para crear registros, implemente un objeto ILogger<TCategoryName> 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;
}

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

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


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 habilita los proveedores de registro de la consola y de depuración


con una llamada al método de extensión CreateDefaultBuilder en Program.cs:

public static void Main(string[] args)


{
BuildWebHost(args).Run();
}

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


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

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, instale su paquete NuGet y llame al método de extensión del proveedor en una
instancia de ILoggerFactory, como se muestra en el ejemplo siguiente:

public void Configure(IApplicationBuilder app,


IHostingEnvironment env,
ILoggerFactory loggerFactory)
{
loggerFactory
.AddConsole()
.AddDebug();

La inserción de dependencias (DI) de ASP.NET Core proporciona la instancia de ILoggerFactory . Los


métodos de extensión AddConsole y AddDebug se definen en los paquetes
Microsoft.Extensions.Logging.Console y Microsoft.Extensions.Logging.Debug. Cada método de extensión
llama al método ILoggerFactory.AddProvider , pasando una instancia del proveedor.
NOTE
La aplicación de ejemplo 1.x agrega proveedores de registro en el método Startup.Configure . Si quiere obtener la
salida de registro de código que se ejecuta antes, agregue los proveedores de registro en el constructor de la clase
Startup .

Obtenga más información sobre los proveedores de registro integrados y encuentre proveedores de registro
de terceros más adelante en el artículo.

Configuración
Uno o varios proveedores de configuración proporcionan la configuración del proveedor de registro:
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).
Por ejemplo, la sección Logging de archivos de configuración de aplicación suele proporcionar la
configuración de registro. En el ejemplo siguiente se muestra el contenido de un archivo
appsettings.Development.json típico:

{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
},
"Console":
{
"IncludeScopes": true
}
}
}

Las claves LogLevel representan los nombres de registro. La clave Default se aplica a los registros que no
se enumeran de forma explícita. El valor representa el nivel de registro aplicado al registro determinado. Las
claves de registro que establecen IncludeScopes ( Console en el ejemplo), especifican si los ámbitos de
registro están habilitados para el registro indicado.

{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
}
}

Las claves LogLevel representan los nombres de registro. La clave Default se aplica a los registros que no
se enumeran de forma explícita. El valor representa el nivel de registro aplicado al registro determinado.
Para obtener información sobre cómo implementar proveedores de configuración, consulte Configuración en
ASP.NET Core.

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("TodoApiSample.Controllers.TodoController");
}

public class TodoController : Controller


{
private readonly ITodoRepository _todoRepository;
private readonly ILogger _logger;

public TodoController(ITodoRepository todoRepository,


ILoggerFactory logger)
{
_todoRepository = todoRepository;
_logger = logger.CreateLogger("TodoApiSample.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;
}

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

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


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).
Creación de 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
CATEGORÍAS QUE
NÚMERO PROVEEDOR COMIENZAN POR... NIVEL DE REGISTRO MÍNIMO

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 == "TodoApiSample.Controllers.TodoController")
{
return false;
}
return true;
});
})
.Build();

Algunos proveedores de registro permiten especificar cuándo deben escribirse los registros en un medio de
almacenamiento o ignorarse en función de la categoría y el nivel de registro.
Los métodos de extensión AddConsole y AddDebug proporcionan sobrecargas que le permiten pasar criterios
de filtrado. El código de ejemplo siguiente hace que el proveedor de la consola ignore los registros por debajo
del nivel Warning , mientras que el proveedor de depuración omite los registros creados por la plataforma.

public void Configure(IApplicationBuilder app,


IHostingEnvironment env,
ILoggerFactory loggerFactory)
{
loggerFactory
.AddConsole(LogLevel.Warning)
.AddDebug((category, logLevel) => (category.Contains("TodoApi") && logLevel >= LogLevel.Trace));

El método AddEventLog tiene una sobrecarga que toma una instancia de EventLogSettings , que puede
contener una función de filtrado en su propiedad Filter . El proveedor de TraceSource no proporciona
ninguna de estas sobrecargas, dado que su nivel de registro y otros parámetros se basan en el SourceSwitch
y TraceListener que usa.
Puede establecer reglas de filtrado para todos los proveedores que están registrados con un instancia de
ILoggerFactory mediante el método de extensión WithFilter . En el ejemplo siguiente se limitan los registros
de la plataforma (la categoría comienza con "Microsoft" o "System") a las advertencias, mientras se permite el
registro de la aplicación en el nivel de depuración.
public void Configure(IApplicationBuilder app,
IHostingEnvironment env,
ILoggerFactory loggerFactory)
{
loggerFactory
.WithFilter(new FilterLoggerSettings
{
{ "Microsoft", LogLevel.Warning },
{ "System", LogLevel.Warning },
{ "ToDoApi", LogLevel.Debug }
})
.AddConsole()
.AddDebug();

Si quiere usar el filtrado para impedir que se escriban todos los registros para una categoría concreta, puede
especificar LogLevel.None como el nivel de registro mínimo de esa categoría. El valor entero de
LogLevel.None es 6, que es superior a LogLevel.Critical (5 ).

El paquete NuGet Microsoft.Extensions.Logging.Filter proporciona el método de extensión WithFilter . El


método devuelve una instancia nueva de ILoggerFactory que filtrará los mensajes de registro pasados a
todos los proveedores de registrador registrados con ella. No afecta a ninguna otra instancia de
ILoggerFactory , incluida la instancia de ILoggerFactory original.

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


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.
Para obtener información sobre la configuración, consulte la sección Configuración.

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.

Startup.cs:

public void Configure(IApplicationBuilder app,


IHostingEnvironment env,
ILoggerFactory loggerFactory)
{
loggerFactory
.AddConsole(includeScopes: true)
.AddDebug();

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.

logging.AddConsole();

loggerFactory.AddConsole();

Las sobrecargas de AddConsole permiten pasar un nivel de registro mínimo, una función de filtro y un valor
booleano que indica si se admiten los ámbitos. Otra opción consiste en pasar un objeto IConfiguration , que
puede especificar niveles de registro y si se admiten los ámbitos.
Si está pensando en la posibilidad de usar la consola de proveedor en producción, tenga en cuenta que tiene
un impacto significativo en el rendimiento.
Cuando se crea un proyecto en Visual Studio, el método AddConsole tiene el aspecto siguiente:

loggerFactory.AddConsole(Configuration.GetSection("Logging"));

Este código hace referencia a la sección Logging del archivo appSettings.json:

{
"Logging": {
"Console": {
"IncludeScopes": false
},
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
}
}

La configuración que se muestra limita los registros de la plataforma a las advertencias mientras que permite
a la aplicación registrar en el nivel de depuración, como se explica en la sección Filtrado del registro. Para
obtener más información, vea Configuración.
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.

logging.AddDebug();

loggerFactory.AddDebug();

Las sobrecargas de AddDebug permiten pasar un nivel mínimo de registro o una función de filtro.
Proveedor EventSource
Para las aplicaciones que tengan como destino ASP.NET Core 1.1.0 o una versión posterior, 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.

logging.AddEventSourceLogger();

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

logging.AddEventLog();

loggerFactory.AddEventLog();

Las sobrecargas de AddEventLog permiten pasar EventLogSettings o un nivel de registro mínimo.


Proveedor TraceSource
El paquete de proveedor Microsoft.Extensions.Logging.TraceSource usa las bibliotecas y proveedores de
System.Diagnostics.TraceSource.
logging.AddTraceSource(sourceSwitchName);

loggerFactory.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 paquete de proveedor está disponible para aplicaciones cuyo destino sea .NET Core 1.1 o
una versión posterior.
Si el destino es .NET Core, tenga en cuenta lo siguiente:
El paquete de proveedor está incluido en el metapaquete Microsoft.AspNetCore.All de ASP.NET Core,
pero no en el metapaquete Microsoft.AspNetCore.App.
No realice llamadas explícitas 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 o hace referencia al metapaquete Microsoft.AspNetCore.App , agregue el
paquete de proveedor al proyecto. Invoque AddAzureWebAppDiagnostics en una instancia ILoggerFactory:

logging.AddAzureWebAppDiagnostics();

loggerFactory.AddAzureWebAppDiagnostics();

Una sobrecarga de AddAzureWebAppDiagnostics permite pasar AzureAppServicesDiagnosticsSettings, con


lo que se puede invalidar la configuración predeterminada, como la plantilla de salida del registro, el nombre
de blob y el límite de tamaño de archivo. (La plantilla de salida es una plantilla de mensaje que se aplica a
todos los registros además de la que se proporciona cuando se llama a un método ILogger ).
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)
KissLog.net (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 .
Registro de seguimiento de Azure Application Insights
El SDK de Application Insights es capaz de recopilar información de telemetría de seguimiento de registros
generados mediante la infraestructura de registro de ASP.NET Core. Para obtener más información, vea
Application Insights para ASP.NET Core y la wiki de Microsoft/ApplicationInsights-aspnetcore sobre el
registro.

Recursos adicionales
Registro de alto rendimiento con LoggerMessage en ASP.NET Core
Registro de alto rendimiento con LoggerMessage en
ASP.NET Core
29/06/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 en comparación con 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 la sección del registrador de la consola de appsettings.json:

{
"Logging": {
"Console": {
"IncludeScopes": true
},
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*"
}

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)

Recursos adicionales
Registro
Controlar errores en ASP.NET Core
12/09/2018 • 14 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, use la página de excepciones para desarrolladores. La página está disponible mediante el paquete
Microsoft.AspNetCore.Diagnostics, a su vez disponible en el metapaquete Microsoft.AspNetCore.App. Agregue
una línea al método Startup.Configure :
Para configurar una aplicación de modo que muestre una página con información detallada sobre las
excepciones, use la página de excepciones para desarrolladores. La página está disponible mediante el paquete
Microsoft.AspNetCore.Diagnostics, a su vez disponible en el metapaquete Microsoft.AspNetCore.All. Agregue
una línea al método Startup.Configure :
Para configurar una aplicación de modo que muestre una página con información detallada sobre las
excepciones, use la página de excepciones para desarrolladores. La página está disponible mediante la adición
de una referencia de paquete para el paquete Microsoft.AspNetCore.Diagnostics en el archivo de proyecto.
Agregue una línea al método Startup.Configure :

public void Configure(IApplicationBuilder app, IHostingEnvironment env)


{
env.EnvironmentName = EnvironmentName.Production;

if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/error");
}

Realice una llamada a UseDeveloperExceptionPage delante de cualquier middleware 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:

Si la solicitud tiene cookies, aparecen en la pestaña Cookies. Los encabezados se ven en la última pestaña:
Configurar una página personalizada de control de excepciones
Configure 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)


{
env.EnvironmentName = EnvironmentName.Production;

if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/error");
}

En una aplicación de Razor Pages, la plantilla de Razor Pages dotnet new proporciona una página de error y una
clase PageModel de error en la carpeta Pages.
En una aplicación MVC, no decore el método de acción del controlador de errores con atributos de método
HTTP, como HttpGet . Los verbos explícitos impiden que algunas solicitudes lleguen al método. Permita el
acceso anónimo al método para que los usuarios no autenticados puedan recibir y ver el error.
Por ejemplo, la plantilla de MVC dotnet new proporciona el siguiente método de controlador de errores y
aparece en el controlador Home:
[AllowAnonymous]
public IActionResult Error()
{
return View(new ErrorViewModel
{ RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}

Configurar 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 proporcionar páginas de códigos de estado, use el
middleware de páginas de código de estado.
El middleware está disponible mediante el paquete Microsoft.AspNetCore.Diagnostics, a su vez disponible en el
metapaquete Microsoft.AspNetCore.App.
El middleware está disponible mediante el paquete Microsoft.AspNetCore.Diagnostics, a su vez disponible en el
metapaquete Microsoft.AspNetCore.All.
El middleware está disponible mediante la adición de una referencia de paquete para el paquete
Microsoft.AspNetCore.Diagnostics en el archivo de proyecto.
Agregue una línea al método Startup.Configure :

app.UseStatusCodePages();

Hay que llamar a UseStatusCodePages antes de los middleware que administran las solicitudes en la
canalización (por ejemplo, middleware de archivos estáticos y middleware de MVC ).
El middleware de páginas de código de estado agrega de forma predeterminada controladores 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:

// Expose the members of the 'Microsoft.AspNetCore.Http' namespace


// at the top of the file:
// using Microsoft.AspNetCore.Http;
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 un código de estado 302 Found al cliente y lo redirige a la plantilla de la dirección URL
de la ubicación facilitada. La plantilla puede incluir un marcador de posición {0} relativo al código de estado.
Las direcciones URL que empiezan por ~ tienen la ruta de acceso base antepuesta. Las direcciones URL que no
empiezan por ~ se usan tal cual.

app.UseStatusCodePagesWithRedirects("/error/{0}");

El método de reejecución devuelve el código de estado original al cliente y especifica que el cuerpo de la
respuesta se debe generar volviendo a ejecutar la canalización de la solicitud mediante una ruta de acceso
alternativa. Es posible que esta ruta de acceso contenga un marcador de posición {0} relativo al código de
estado:

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

Si se usa una sobrecarga UseStatusCodePages* que apunta a un punto de conexión dentro de la aplicación, cree
una vista de MVC o una página de Razor Pages para ese punto de conexión. Por ejemplo, la plantilla dotnet new
de una aplicación de Razor Pages genera la página y la clase de modelo de página siguientes:
Error.cshtml:
@page
@model ErrorModel
@{
ViewData["Title"] = "Error";
}

<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>

@if (Model.ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@Model.RequestId</code>
</p>
}

<h3>Development Mode</h3>
<p>
Swapping to <strong>Development</strong> environment will display more detailed
information about the error that occurred.
</p>
<p>
<strong>Development environment should not be enabled in deployed applications
</strong>, as it can result in sensitive information from exceptions being
displayed to end users. For local debugging, development environment can be
enabled by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment
variable to <strong>Development</strong>, and restarting the application.
</p>

Error.cshtml.cs:

public class ErrorModel : PageModel


{
public string RequestId { get; set; }

public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None,


NoStore = true)]
public void OnGet()
{
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
}
}

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. Para obtener información sobre cómo solucionar problemas de inicio al hospedar
con IIS, vea Solución de problemas de ASP.NET Core en IIS. Para obtener información sobre cómo solucionar
problemas de inicio con Azure App Service, vea Solución de problemas de ASP.NET Core en Azure App Service.

Control de errores de ASP.NET Core 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 llama a estos filtros. Para obtener
más información, vea 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 middleware de control de errores. Es recomendable dar preferencia al uso de middleware y usar
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 optan 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.

Recursos adicionales
Referencia de errores comunes de Azure App Service e IIS con ASP.NET Core
Solución de problemas de ASP.NET Core en IIS
Solución de problemas de ASP.NET Core en Azure App Service
Hospedaje en ASP.NET Core
05/09/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
IWebHostBuilder. Para hospedar aplicaciones que no sean web, los desarrolladores deben usar el host
genérico basado en HostBuilder.
Tareas en segundo plano con servicios hospedados en ASP.NET Core
Obtenga información sobre cómo implementar tareas en segundo plano con servicios hospedados en
ASP.NET Core.
Mejorar una aplicación desde un ensamblado externo en ASP.NET Core con IHostingStartup
Descubra cómo mejorar una aplicación ASP.NET Core desde un ensamblado sin referencia o al que se hace
referencia mediante una implementación de IHostingStartup.
Host web de ASP.NET Core
12/09/2018 • 34 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 (IWebHostBuilder), muy útil
para hospedar aplicaciones web. Para conocer la cobertura del host genérico de .NET ( IHostBuilder), vea Host
genérico de .NET.

Configuración de un host
Cree un host con una instancia de IWebHostBuilder. 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 y configura el servidor por medio de los proveedores de
configuración de hospedaje de la aplicación. Para consultar las opciones predeterminadas de Kestrel, vea
Implementación del servidor web Kestrel en ASP.NET Core.
Establece la raíz de contenido en la ruta de acceso devuelta por Directory.GetCurrentDirectory.
Carga la configuración de host de:
Variables de entorno con el prefijo ASPNETCORE_ (por ejemplo, ASPNETCORE_ENVIRONMENT ).
Argumentos de la línea de comandos.
Carga la configuración de aplicación de:
appsettings.json.
appsettings.{Environment}.json.
Secretos del usuario cuando la aplicación se ejecuta en el entorno Development por medio del
ensamblado de entrada.
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 consultar las opciones
predeterminadas de IIS, vea 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 configuración definida en CreateDefaultBuilder se puede reemplazar y aumentar mediante
ConfigureAppConfiguration, ConfigureLogging y otros métodos y métodos de extensión de IWebHostBuilder.
A continuación, se presentan algunos ejemplos:
ConfigureAppConfiguration se usa para especificar IConfiguration adicionales para la aplicación. La
siguiente llamada ConfigureAppConfiguration agrega un delegado para incluir la configuración de la
aplicación en el archivo appsettings.xml. Es posible llamar a ConfigureAppConfiguration varias veces.
Tenga en cuenta que esta configuración no se aplica al host (por ejemplo, direcciones URL de servidor o
entorno). Vea la sección Valores de configuración de host.

WebHost.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((hostingContext, config) =>
{
config.AddXmlFile("appsettings.xml", optional: true, reloadOnChange: true);
})
...

La siguiente llamada ConfigureLogging agrega un delegado para configurar el nivel de registro mínimo
(SetMinimumLevel) en LogLevel.Warning. Esta configuración reemplaza la configuración de
appsettings.Development.JSON ( LogLevel.Debug ) y appsettings.Production.JSON ( LogLevel.Error )
configurada mediante CreateDefaultBuilder . Es posible llamar a ConfigureLogging varias veces.

WebHost.CreateDefaultBuilder(args)
.ConfigureLogging(logging =>
{
logging.SetMinimumLevel(LogLevel.Warning);
})
...

La siguiente llamada a ConfigureKestrel reemplaza el valor predeterminado de


Limits.MaxRequestBodySize de 30 000 000 bytes establecido al configurar Kestrel mediante
CreateDefaultBuilder :

WebHost.CreateDefaultBuilder(args)
.ConfigureKestrel((context, options) =>
{
options.Limits.MaxRequestBodySize = 20000000;
});
...

La siguiente llamada a UseKestrel reemplaza el valor predeterminado de Limits.MaxRequestBodySize de


30 000 000 bytes establecido al configurar Kestrel mediante CreateDefaultBuilder :

WebHost.CreateDefaultBuilder(args)
.UseKestrel(options =>
{
options.Limits.MaxRequestBodySize = 20000000;
});
...
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 obtener más información sobre la configuración de la aplicación, vea 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.

Cree un host con una instancia de WebHostBuilder. La creación de un host se suele realizar en el punto de
entrada de la aplicación, el método Main . En las plantillas de proyecto, Main se encuentra en Program.cs:

public class Program


{
public static void Main(string[] args)
{
BuildWebHost(args).Run();
}

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


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

WebHostBuilder requiere un servidor que implemente IServer. Los servidores integrados son Kestrel y HTTP.sys
(antes del lanzamiento de ASP.NET Core 2.0, HTTP.sys se llamaba WebListener). En este ejemplo, el método de
extensión UseKestrel especifica el servidor Kestrel.
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. La raíz de contenido predeterminada se obtiene para UseContentRoot mediante
Directory.GetCurrentDirectory. 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 utilizar IIS como un proxy inverso, llame a UseIISIntegration como parte de la creación del host. A
diferencia de UseKestrel, UseIISIntegration no configura un servidor. UseIISIntegration configura la ruta de
acceso base y el puerto que escucha el servidor cuando se usa el módulo ASP.NET Core para crear un proxy
inverso entre Kestrel e IIS. Para utilizar IIS con ASP.NET Core, deben especificarse UseKestrel y
UseIISIntegration . UseIISIntegration solo se activa cuando se ejecuta en segundo plano de IIS o IIS Express.
Para obtener más información, consulte Módulo ASP.NET Core y Referencia de configuración del módulo
ASP.NET Core.
Una implementación mínima que configura un host (y una aplicación de ASP.NET Core) tendrá que especificar
un servidor y la configuración de la canalización de solicitudes de la aplicación:

var host = new WebHostBuilder()


.UseKestrel()
.Configure(app =>
{
app.Run(context => context.Response.WriteAsync("Hello World!"));
})
.Build();

host.Run();
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 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 .
Extensiones como UseContentRoot y UseConfiguration (vea la sección Invalidación de la configuración).
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.
Clave de aplicación (nombre )
La propiedad IHostingEnvironment.ApplicationName se establece automáticamente cuando se llama a
UseStartup o Configure durante la construcción del host. El valor se establece en el nombre del ensamblado que
contiene el punto de entrada de la aplicación. Para establecer el valor explícitamente, use
WebHostDefaults.ApplicationKey:
Clave: nombreDeAplicación
Tipo: cadena
Valor predeterminado: nombre del ensamblado que contiene el punto de entrada de la aplicación.
Establecer mediante: UseSetting
Variable de entorno: ASPNETCORE_APPLICATIONKEY

WebHost.CreateDefaultBuilder(args)
.UseSetting(WebHostDefaults.ApplicationKey, "CustomApplicationName")

var host = new WebHostBuilder()


.UseSetting("applicationName", "CustomApplicationName")

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.

WebHost.CreateDefaultBuilder(args)
.CaptureStartupErrors(true)
var host = new WebHostBuilder()
.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.

WebHost.CreateDefaultBuilder(args)
.UseContentRoot("c:\\<content-root>")

var host = new WebHostBuilder()


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

WebHost.CreateDefaultBuilder(args)
.UseSetting(WebHostDefaults.DetailedErrorsKey, "true")

var host = new WebHostBuilder()


.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, vea Usar varios entornos en
ASP.NET Core.

WebHost.CreateDefaultBuilder(args)
.UseEnvironment(EnvironmentName.Development)

var host = new WebHostBuilder()


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

WebHost.CreateDefaultBuilder(args)
.UseSetting(WebHostDefaults.HostingStartupAssembliesKey, "assembly1;assembly2")

Puerto HTTPS
Establezca puerto de redirección HTTPS. Se usa en Exigir HTTPS.
Clave: https_port Tipo: string Valor predeterminada: no se establece un valor predeterminado. Establecer
mediante: UseSetting Variable de entorno: ASPNETCORE_HTTPS_PORT

WebHost.CreateDefaultBuilder(args)
.UseSetting("https_port", "8080")

Ensamblados de exclusión de inicio de hospedaje


DESCRIPTION
Clave: hostingStartupExcludeAssemblies
Tipo: cadena
Valor predeterminado: una cadena vacía
Establecer mediante: UseSetting
Variable de entorno: ASPNETCORE_HOSTINGSTARTUPEXCLUDEASSEMBLIES
Una cadena delimitada por punto y coma de ensamblados de inicio de hospedaje para excluir en el inicio.

WebHost.CreateDefaultBuilder(args)
.UseSetting(WebHostDefaults.HostingStartupExcludeAssembliesKey, "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

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 obtener más información, vea Mejorar una aplicación desde un ensamblado
externo en ASP.NET Core con IHostingStartup.
Clave: preventHostingStartup
Tipo: bool ( true o 1 )
Valor predeterminado: false
Establecer mediante: UseSetting
Variable de entorno: ASPNETCORE_PREVENTHOSTINGSTARTUP

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.

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 obtener más información, vea
Implementación del servidor web Kestrel en ASP.NET Core.

var host = new WebHostBuilder()


.UseUrls("http://*:5000;http://localhost:5001;https://hostname:5002")
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 (por ejemplo,
UseSetting
.UseSetting(WebHostDefaults.ShutdownTimeoutKey, "10") ), el método de extensión UseShutdownTimeout toma
TimeSpan.
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.

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.

WebHost.CreateDefaultBuilder(args)
.UseStartup("StartupAssemblyName")

WebHost.CreateDefaultBuilder(args)
.UseStartup<TStartup>()

var host = new WebHostBuilder()


.UseStartup("StartupAssemblyName")

var host = new WebHostBuilder()


.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

WebHost.CreateDefaultBuilder(args)
.UseWebRoot("public")

var host = new WebHostBuilder()


.UseWebRoot("public")

Invalidación de la configuración
Use Configuración para configurar el host web. En el ejemplo siguiente, la configuración del host se especifica
de forma opcional en un archivo hostsettings.json. Cualquier configuración que se cargue del archivo
hostsettings.json se puede reemplazar por argumentos de línea de comandos. La configuración generada (en
config ) se usa para configurar el host con UseConfiguration. La configuración de IWebHostBuilder se agrega a
la de la aplicación, pero lo contrario no es aplicable: ConfigureAppConfiguration no afecta a la configuración de
IWebHostBuilder .

Reemplazar primero la configuración proporcionada por UseUrls por la de hostsettings.json y, luego, por la del
argumento de línea de comandos:

public class Program


{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}

public static IWebHostBuilder CreateWebHostBuilder(string[] args)


{
var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("hostsettings.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();
}
}

hostsettings.json:
{
urls: "http://*:5005"
}

Reemplazar primero la configuración proporcionada por UseUrls por la de hostsettings.json y, luego, por la del
argumento de línea de comandos:

public class Program


{
public static void Main(string[] args)
{
var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("hostsettings.json", optional: true)
.AddCommandLine(args)
.Build();

var host = new WebHostBuilder()


.UseUrls("http://*:5000")
.UseConfiguration(config)
.UseKestrel()
.Configure(app =>
{
app.Run(context =>
context.Response.WriteAsync("Hello, World!"));
})
.Build();

host.Run();
}
}

hostsettings.json:

{
urls: "http://*:5005"
}

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).
UseConfiguration solo copia las claves del elemento IConfiguration proporcionado a la configuración del generador
de host. Por consiguiente, el hecho de configurar reloadOnChange: true para los archivos de configuración XML, JSON e
INI no tiene ningún efecto.

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 hostsettings.json, y el servidor efectúa la escucha en el puerto 8080:
dotnet run --urls "http://*:8080"

Administración del host


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

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 obtener más información, vea Usar varios entornos
en ASP.NET Core.

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


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


Lo siguiente solo es pertinente en las aplicaciones de ASP.NET Core 2.0 cuando la aplicación no llama
a UseStartup o Configure .
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 es necesario el nombre de la aplicación (el nombre del ensamblado actual) 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", "AssemblyName")

O bien, agregue un Configure ficticio a WebHostBuilder , que establece el nombre de la aplicación


automáticamente:

WebHost.CreateDefaultBuilder(args)
.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 de ASP.NET Core en Windows con IIS
Hospedar ASP.NET Core en Linux con Nginx
Hospedar ASP.NET Core en Linux con Apache
Hospedaje de ASP.NET Core en un servicio de Windows
Host genérico de .NET
12/09/2018 • 20 minutes to read • Edit Online

Por Luke Latham


Las aplicaciones de .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. 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), vea Host web de ASP.NET Core.
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. El paquete Microsoft.Extensions.Hosting está incluido en
el metapaquete Microsoft.AspNetCore.App (ASP.NET Core 2.1 o posterior).
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.
La configuración de las variables de entorno no se agrega de forma predeterminada. Llame a
AddEnvironmentVariables en el generador de host para configurar el host a partir de las variables de entorno.
AddEnvironmentVariables acepta un prefijo opcional definido por el usuario. La aplicación de ejemplo usa el
prefijo PREFIX_ . El prefijo se quita cuando se leen las variables de entorno. Al configurar el host de la aplicación
de ejemplo, el valor de la variable de entorno de PREFIX_ENVIRONMENT se convierte en el valor de configuración de
host de la clave environment .
Durante el desarrollo, al usar Visual Studio o al ejecutar una aplicación con dotnet run , se pueden establecer
variables de entorno en el archivo Properties/launchSettings.json. En Visual Studio Code, las variables de
entorno se pueden establecer en el archivo .vscode/launch.json durante el desarrollo. Para obtener más
información, vea Usar varios entornos en ASP.NET Core.
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.SetBasePath(Directory.GetCurrentDirectory());
configHost.AddJsonFile("hostsettings.json", optional: true);
configHost.AddEnvironmentVariables(prefix: "PREFIX_");
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.
Clave de aplicación (nombre)
La propiedad IHostingEnvironment.ApplicationName se establece desde la configuración del host durante la
construcción de este. Para establecer el valor explícitamente, use HostDefaults.ApplicationKey:
Clave: nombreDeAplicación
Tipo: cadena
Valor predeterminado: nombre del ensamblado que contiene el punto de entrada de la aplicación.
Establecer mediante: HostBuilderContext.HostingEnvironment.ApplicationName
Variable de entorno: <PREFIX_>APPLICATIONKEY ( <PREFIX_> es opcional y definido por el usuario)

var host = new HostBuilder()


.ConfigureAppConfiguration((hostContext, configApp) =>
{
hostContext.HostingEnvironment.ApplicationName = "CustomApplicationName";
})

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: <PREFIX_>CONTENTROOT ( <PREFIX_> es opcional y definido por el usuario)
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: <PREFIX_>ENVIRONMENT ( <PREFIX_> es opcional y definido por el usuario)
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.

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.SetBasePath(Directory.GetCurrentDirectory());
configApp.AddJsonFile("appsettings.json", optional: true);
configApp.AddJsonFile(
$"appsettings.{hostContext.HostingEnvironment.EnvironmentName}.json",
optional: true);
configApp.AddEnvironmentVariables(prefix: "PREFIX_");
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).

Para mover los archivos de configuración al directorio de salida, especifique los archivos de configuración como
elementos de proyecto de MSBuild en el archivo de proyecto. La aplicación de ejemplo mueve sus archivos de
configuración de aplicación JSON y hostsettings.json con el elemento <Content:> siguiente:

<ItemGroup>
<Content Include="**\*.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

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 obtener más información, vea Tareas en segundo plano con servicios hospedados en
ASP.NET Core.
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<ServiceContainer>
{
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 ServiceContainerFactory())
.ConfigureContainer<ServiceContainer>((hostContext, container) =>
{
})

Extensibilidad
La extensibilidad de host se realiza con métodos de extensión en IHostBuilder . En el ejemplo siguiente se
muestra cómo un método de extensión extiende una implementación de IHostBuilder con el ejemplo
TimedHostedService demostrado en Tareas en segundo plano con servicios hospedados en ASP.NET Core.

var host = new HostBuilder()


.UseHostedService<TimedHostedService>()
.Build();

await host.StartAsync();

Una aplicación establece el método de extensión UseHostedService para registrar el servicio hospedado pasado
en T :

using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

public static class Extensions


{
public static IHostBuilder UseHostedService<T>(this IHostBuilder hostBuilder)
where T : class, IHostedService, IDisposable
{
return hostBuilder.ConfigureServices(services =>
services.AddHostedService<T>());
}
}

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, vea Usar varios entornos en ASP.NET Core.

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.


TOKEN DE CANCELACIÓN SE DESENCADENA CUANDO…

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. En la aplicación de ejemplo se usa
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 en ASP.NET Core
Ejemplos de hospedaje de repositorios en GitHub
Tareas en segundo plano con servicios hospedados
en ASP.NET Core
07/09/2018 • 8 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) - StartAsync contiene la lógica para iniciar la tarea en segundo plano. Al
utilizar el host web, se llama a StartAsync después de que el servidor se haya iniciado y se haya activado
IApplicationLifetime.ApplicationStarted. Al utilizar el host genérico, se llama a StartAsync antes de que se
desencadene ApplicationStarted .
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 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 con el método de extensión AddHostedService :

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 . La implementación IHostedService se registra con el


método de extensión AddHostedService :

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 3.0):
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 en la cola se quitan de la cola y se ejecutan como un servicio
BackgroundService, que es una clase base para implementar IHostedService de ejecución prolongada:
public class QueuedHostedService : BackgroundService
{
private readonly ILogger _logger;

public QueuedHostedService(IBackgroundTaskQueue taskQueue,


ILoggerFactory loggerFactory)
{
TaskQueue = taskQueue;
_logger = loggerFactory.CreateLogger<QueuedHostedService>();
}

public IBackgroundTaskQueue TaskQueue { get; }

protected async override Task ExecuteAsync(


CancellationToken cancellationToken)
{
_logger.LogInformation("Queued Hosted Service is starting.");

while (!cancellationToken.IsCancellationRequested)
{
var workItem = await TaskQueue.DequeueAsync(cancellationToken);

try
{
await workItem(cancellationToken);
}
catch (Exception ex)
{
_logger.LogError(ex,
$"Error occurred executing {nameof(workItem)}.");
}
}

_logger.LogInformation("Queued Hosted Service is stopping.");


}
}

Los servicios se registran en Startup.ConfigureServices . La implementación IHostedService se registra con el


método de extensión AddHostedService :

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
Mejorar una aplicación desde un ensamblado
externo en ASP.NET Core con IHostingStartup
07/09/2018 • 28 minutes to read • Edit Online

Por Luke Latham


Una implementación de IHostingStartup (inicio de hospedaje) permite agregar mejoras a una aplicación al
iniciarla a partir de un ensamblado externo. Por ejemplo, una biblioteca externa puede usar una implementación
de inicio de hospedaje para suministrar más servicios o proveedores de configuración a una aplicación.
IHostingStartup está disponible en ASP.NET Core 2.0 o versiones posteriores.

Vea o descargue el código de ejemplo (cómo descargarlo)

Atributo HostingStartup
Un atributo HostingStartup indica la presencia de un ensamblado de inicio de hospedaje para activar en tiempo
de ejecución.
El ensamblado de entrada o el ensamblado que contiene la clase Startup se analiza automáticamente para
detectar el atributo HostingStartup . La lista de ensamblados para buscar atributos HostingStartup se carga en
tiempo de ejecución desde la configuración de WebHostDefaults.HostingStartupAssembliesKey. La lista de
ensamblados que se excluirán de la detección se carga desde
WebHostDefaults.HostingStartupExcludeAssembliesKey. Para obtener más información, vea Host web:
ensamblados de inicio de hospedaje y Web Host: Hosting Startup Exclude Assemblies (Host web: ensamblados
de exclusión de inicio de hospedaje).
En el ejemplo siguiente, el espacio de nombres del ensamblado de inicio de hospedaje es StartupEnhancement .
La clase que contiene el código de inicio de hospedaje es StartupEnhancementHostingStartup :

[assembly: HostingStartup(typeof(StartupEnhancement.StartupEnhancementHostingStartup))]

Normalmente el atributo HostingStartup se encuentra en el archivo de clase de implementación


IHostingStartup del ensamblado de inicio de hospedaje.

Detectar los ensamblados de inicio de hospedaje cargados


Para detectar los ensamblados de inicio de hospedaje cargados, 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.

Deshabilitar la carga automática de ensamblados de inicio de


hospedaje
Para deshabilitar la carga automática de los ensamblados de inicio de hospedaje, use uno de los enfoques
siguientes:
Para evitar que se carguen todos los ensamblados de inicio de hospedaje, establezca uno de los
procedimientos siguientes en true o 1 :
La opción de configuración de hospedaje para Evitar el inicio de hospedaje.
La variable de entorno ASPNETCORE_PREVENTHOSTINGSTARTUP .
Para evitar la carga de ensamblados de inicio de hospedaje específicos, establezca una de las opciones
siguientes en una cadena delimitada por punto y coma de ensamblados de inicio de hospedaje para excluir
durante el inicio:
La opción de configuración de host para Ensamblados de exclusión de inicio de hospedaje.
La variable de entorno ASPNETCORE_HOSTINGSTARTUPEXCLUDEASSEMBLIES .

Para deshabilitar la carga automática de los ensamblados de inicio de hospedaje, establezca una de las opciones
siguientes en true o 1 :
La opción de configuración de hospedaje para Evitar el inicio de hospedaje.
La variable de entorno ASPNETCORE_PREVENTHOSTINGSTARTUP .
Si se establecen la opción de configuración de host y la variable de entorno, la configuración de host controla 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 el ensamblado se deshabilite globalmente y, asimismo, puede deshabilitar también otras
características de una aplicación.

Proyecto
Cree un inicio de hospedaje con cualquiera de los siguientes tipos de proyecto:
Biblioteca de clases
Aplicación de consola sin un punto de entrada
Biblioteca de clases
Una mejora de inicio de hospedaje se puede proporcionar en una biblioteca de clases. La biblioteca contiene un
atributo HostingStartup .
El código de ejemplo incluye una aplicación Razor Pages, HostingStartupApp y una biblioteca de clases,
HostingStartupLibrary. La biblioteca de clases:
Contiene una clase de inicio de hospedaje, ServiceKeyInjection , que implementa IHostingStartup .
ServiceKeyInjection agrega un par de cadenas de servicio a la configuración de la aplicación mediante el
proveedor de configuración en memoria (AddInMemoryCollection).
Incluye un atributo HostingStartup que identifica espacio de nombres y la clase del inicio de hospedaje.
El método Configure de la clase ServiceKeyInjection usa un IWebHostBuilder para agregar mejoras a una
aplicación. El tiempo de ejecución llama a IHostingStartup.Configure en el ensamblado de inicio del hospedaje
antes de Startup.Configure en el código de usuario, lo que permite que el código de usuario sobrescriba
cualquier configuración facilitada por el ensamblado de inicio del hospedaje.
HostingStartupLibrary/ServiceKeyInjection.cs:
[assembly: HostingStartup(typeof(HostingStartupLibrary.ServiceKeyInjection))]

namespace HostingStartupLibrary
{
public class ServiceKeyInjection : IHostingStartup
{
public void Configure(IWebHostBuilder builder)
{
builder.ConfigureAppConfiguration(config =>
{
var dict = new Dictionary<string, string>
{
{"DevAccount_FromLibrary", "DEV_1111111-1111"},
{"ProdAccount_FromLibrary", "PROD_2222222-2222"}
};

config.AddInMemoryCollection(dict);
});
}
}
}

La página de índice de la aplicación lee y procesa los valores de configuración para las dos claves establecidas
por el ensamblado de inicio de hospedaje de la biblioteca de clases:
HostingStartupApp/Pages/Index.cshtml.cs:

public class IndexModel : PageModel


{
public IndexModel(IConfiguration config)
{
ServiceKey_Development_Library = config["DevAccount_FromLibrary"];
ServiceKey_Production_Library = config["ProdAccount_FromLibrary"];
ServiceKey_Development_Package = config["DevAccount_FromPackage"];
ServiceKey_Production_Package = config["ProdAccount_FromPackage"];
}

public string ServiceKey_Development_Library { get; private set; }


public string ServiceKey_Production_Library { get; private set; }
public string ServiceKey_Development_Package { get; private set; }
public string ServiceKey_Production_Package { get; private set; }

public void OnGet()


{
}
}

El código de ejemplo también incluye un proyecto de paquete NuGet que proporciona un inicio de hospedaje
independiente, HostingStartupPackage. El paquete tiene las mismas características que la biblioteca de clases
que se ha descrito anteriormente. El paquete:
Contiene una clase de inicio de hospedaje, ServiceKeyInjection , que implementa IHostingStartup .
ServiceKeyInjection agrega un par de cadenas de servicio a la configuración de la aplicación.
Incluye un atributo HostingStartup .
HostingStartupPackage/ServiceKeyInjection.cs:
[assembly: HostingStartup(typeof(HostingStartupPackage.ServiceKeyInjection))]

namespace HostingStartupPackage
{
public class ServiceKeyInjection : IHostingStartup
{
public void Configure(IWebHostBuilder builder)
{
builder.ConfigureAppConfiguration(config =>
{
var dict = new Dictionary<string, string>
{
{"DevAccount_FromPackage", "DEV_3333333-3333"},
{"ProdAccount_FromPackage", "PROD_4444444-4444"}
};

config.AddInMemoryCollection(dict);
});
}
}
}

La página de índice de la aplicación lee y procesa los valores de configuración para las dos claves establecidas
por el ensamblado de inicio de hospedaje del paquete:
HostingStartupApp/Pages/Index.cshtml.cs:

public class IndexModel : PageModel


{
public IndexModel(IConfiguration config)
{
ServiceKey_Development_Library = config["DevAccount_FromLibrary"];
ServiceKey_Production_Library = config["ProdAccount_FromLibrary"];
ServiceKey_Development_Package = config["DevAccount_FromPackage"];
ServiceKey_Production_Package = config["ProdAccount_FromPackage"];
}

public string ServiceKey_Development_Library { get; private set; }


public string ServiceKey_Production_Library { get; private set; }
public string ServiceKey_Development_Package { get; private set; }
public string ServiceKey_Production_Package { get; private set; }

public void OnGet()


{
}
}

Aplicación de consola sin un punto de entrada


Este enfoque solo está disponible para las aplicaciones de .NET Core, no de .NET Framework.
Se puede proporcionar una mejora del inicio de hospedaje dinámico que no requiere una referencia de tiempo
de compilación para la activación en una aplicación de consola sin un punto de entrada. La aplicación contiene
un atributo HostingStartup . Para crear un inicio de hospedaje dinámico, siga estos pasos:
1. La biblioteca de implementación se crea a partir de la clase que contiene la implementación IHostingStartup
. La biblioteca de implementación se trata como un paquete normal.
2. Una aplicación de consola sin un punto de entrada hace referencia al paquete de la biblioteca de
implementación. Una aplicación de consola se usa porque:
Un archivo de dependencias es un recurso de aplicación ejecutable, por lo que una biblioteca no
puede proporcionar un archivo de dependencias.
Una biblioteca no se puede agregar directamente al almacén de paquetes en tiempo de ejecución, lo
que requiere un proyecto ejecutable que tenga como destino el tiempo de ejecución compartido.
3. La aplicación de consola se publica para obtener las dependencias del inicio de hospedaje. Una consecuencia
de la publicación de la aplicación de consola es que las dependencias no usadas se cortan en el archivo de
dependencias.
4. La aplicación y su archivo de dependencias se colocan en el almacén de paquetes en tiempo de ejecución.
Para detectar el ensamblado de inicio de hospedaje y su archivo de dependencias, se hace referencia a ellos
en un par de variables de entorno.
La aplicación de consola 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.1" />
</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 Configure de la clase usa un IWebHostBuilder para agregar
mejoras a una aplicación. El tiempo de ejecución llama a IHostingStartup.Configure en el ensamblado de inicio
del hospedaje antes de Startup.Configure en el código de usuario, lo que permite que el código de usuario
sobrescriba cualquier configuración facilitada por el ensamblado de inicio del hospedaje.

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.1"
},
"runtime": {
"StartupEnhancement.dll": {}
}
}
}
}

Solo se muestra parte del archivo. El nombre del ensamblado en el ejemplo es StartupEnhancement .

Especificar el ensamblado de inicio de hospedaje


En el caso de un inicio de hospedaje proporcionado por una biblioteca de clase o una aplicación de consola,
especifique el nombre del ensamblado de inicio de hospedaje en la variable de entorno
ASPNETCORE_HOSTINGSTARTUPASSEMBLIES . La variable de entorno es una lista de ensamblados delimitada por punto
y coma.
Solo se examinan los ensamblados de inicio de hospedaje en busca del atributo HostingStartup . En la
aplicación de ejemplo, HostingStartupApp, para descubrir los nuevos inicios de hospedaje descritos
anteriormente, la variable de entorno se establece en el siguiente valor:

HostingStartupLibrary;HostingStartupPackage;StartupDiagnostics

Un ensamblado de inicio de hospedaje también se puede establecer mediante la configuración de host


Ensamblados de inicio de hospedaje.
Cuando existen varios ensamblados de inicio del hospedaje, sus métodos Configure se ejecutan en el orden en
el que aparecen los ensamblados.

Activación
Las opciones de activación del inicio de hospedaje son:
Almacén en tiempo de ejecución – la activación no requiere una referencia de tiempo de compilación para la
activación. La aplicación de ejemplo coloca el ensamblado de inicio de hospedaje y los archivos de
dependencias en una carpeta, implementación, para facilitar la implementación del inicio del hospedaje en
un entorno de varios equipos. La carpeta implementación también incluye un script de PowerShell que crea
o modifica las variables de entorno en el sistema de implementación para habilitar el inicio de hospedaje.
Se requiere una referencia al tiempo de compilación para la activación
Paquete NuGet
Carpeta bin del proyecto
Almacén en tiempo de ejecución
La implementación de inicio de hospedaje se coloca en el almacén en tiempo de ejecución. No es necesario que
la aplicación mejorada haga ninguna referencia de tiempo de compilación al ensamblado.
Una vez compilado el inicio de hospedaje, el archivo de proyecto de inicio de hospedaje actúa como el archivo
de manifiesto para el comando dotnet store.
dotnet store --manifest <PROJECT_FILE> --runtime <RUNTIME_IDENTIFIER>

Este comando coloca el ensamblado de inicio de hospedaje y otras dependencias que no forman parte del
marco compartido en el almacén de tiempo de ejecución del perfil de usuario en:
Windows
macOS
Linux

%USERPROFILE%\.dotnet\store\x64\<TARGET_FRAMEWORK_MONIKER>\<ENHANCEMENT_ASSEMBLY_NAME>\
<ENHANCEMENT_VERSION>\lib\<TARGET_FRAMEWORK_MONIKER>\

Si desea colocar el ensamblado y las dependencias para su uso global, agregue la opción -o|--output al
comando dotnet store con la ruta de acceso siguiente:
Windows
macOS
Linux

%PROGRAMFILES%\dotnet\store\x64\<TARGET_FRAMEWORK_MONIKER>\<ENHANCEMENT_ASSEMBLY_NAME>\
<ENHANCEMENT_VERSION>\lib\<TARGET_FRAMEWORK_MONIKER>\

Modificar y colocar el archivo de dependencias del inicio de hospedaje


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.1"
},
"runtime": {
"lib/netcoreapp2.1/StartupEnhancement.dll": {}
}
}
}
}

En la aplicación de ejemplo (proyecto StartupDiagnostics), 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.
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 de .dotnet del perfil de usuario:
Windows
macOS
Linux
%USERPROFILE%\.dotnet\x64\additionalDeps\<ENHANCEMENT_ASSEMBLY_NAME>\shared\Microsoft.NETCore.App\
<SHARED_FRAMEWORK_VERSION>\

Para usarlo de manera global, colóquelo en la carpeta additonalDeps de la instalación de .NET Core:
Windows
macOS
Linux

%PROGRAMFILES%\dotnet\additionalDeps\<ENHANCEMENT_ASSEMBLY_NAME>\shared\Microsoft.NETCore.App\
<SHARED_FRAMEWORK_VERSION>\

La versión "Shared Framework" 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 (HostingStartupApp), el tiempo de ejecución compartido se especifica en el archivo
HostingStartupApp.runtimeconfig.json.
Indicar el archivo de dependencias del inicio de hospedaje
La ubicación del archivo *.deps.json de implementación aparece en la variable de entorno
DOTNET_ADDITIONAL_DEPS .

Si el archivo se coloca en la carpeta .dotnet del perfil de usuario, establezca el valor de la variable de entorno en:
Windows
macOS
Linux

%USERPROFILE%\.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:
Windows
macOS
Linux

%PROGRAMFILES%\dotnet\additionalDeps\<ENHANCEMENT_ASSEMBLY_NAME>\shared\Microsoft.NETCore.App\
<SHARED_FRAMEWORK_VERSION>\<ENHANCEMENT_ASSEMBLY_NAME>.deps.json

Para que la aplicación de ejemplo (HostingStartupApp) encuentre el archivo de dependencias


(HostingStartupApp.runtimeconfig.json), el archivo de dependencias se coloca en el perfil del usuario.
Windows
macOS
Linux
Use la sintaxis siguiente para establecer la variable de entorno DOTNET_ADDITIONAL_DEPS :

%UserProfile%\.dotnet\x64\additionalDeps\StartupDiagnostics\

Para obtener ejemplos de cómo establecer variables de entorno en distintos sistemas operativos, vea Uso de
varios entornos.
Implementación
Para facilitar la implementación de un inicio de hospedaje en un entorno de varios equipos, la aplicación de
ejemplo crea una carpeta de implementación en el resultado publicado que contiene:
El ensamblado de inicio de hospedaje.
El archivo de dependencias del inicio de hospedaje.
Un script de PowerShell que crea o modifica ASPNETCORE_HOSTINGSTARTUPASSEMBLIES y DOTNET_ADDITIONAL_DEPS
para admitir la activación del inicio del hospedaje. Ejecute el script desde un símbolo del sistema de
PowerShell administrativo en el sistema de implementación.
Detección de
Una mejora de inicio de hospedaje se puede proporcionar en un paquete NuGet. El paquete tiene un atributo
HostingStartup . Los tipos de inicio de hospedaje proporcionados por el paquete están disponibles en la
aplicación mediante cualquiera de los métodos siguientes:
El archivo de proyecto de la aplicación mejorada hace una referencia al paquete para el inicio de hospedaje
en el archivo de proyecto de la aplicación (una referencia de tiempo de compilación). Con la referencia de
tiempo de compilación realizada, el ensamblado de inicio de hospedaje y todas sus dependencias se
incorporan en el archivo de dependencia de la aplicación (*. deps.json). Este enfoque se aplica a un paquete
de ensamblado de inicio de hospedaje publicado en nuget.org.
El archivo de dependencias del inicio de hospedaje está disponible para la aplicación mejorada como se
describe en la sección Almacén en tiempo de ejecución (sin una referencia de tiempo de compilación).
Para obtener más información sobre los paquetes NuGet y el almacén en tiempo de ejecución, vea los temas
siguientes:
Cómo crear un paquete NuGet con herramientas multiplataforma
Publicar paquetes
Runtime package store (Almacenamiento de paquetes en tiempo de ejecución)
Carpeta bin del proyecto
Un ensamblado implementado por bin puede proporcionar una mejora del inicio de hospedaje en la aplicación
mejorada. Los tipos de inicio de hospedaje proporcionados por el ensamblado están disponibles en la aplicación
mediante cualquiera de los métodos siguientes:
El archivo de proyecto de la aplicación mejorada hace referencia de ensamblado al inicio de hospedaje (una
referencia de tiempo de compilación). Con la referencia de tiempo de compilación realizada, el ensamblado
de inicio de hospedaje y todas sus dependencias se incorporan en el archivo de dependencia de la aplicación
(*. deps.json). Este enfoque se aplica cuando el escenario de implementación llama para mover el
ensamblado de la biblioteca de inicio de hospedaje compilado (archivo DLL ) al proyecto de consumo o a una
ubicación a la que el proyecto de consumo pueda acceder y se realiza una referencia de tiempo de
compilación en el ensamblado del inicio de hospedaje.
El archivo de dependencias del inicio de hospedaje está disponible para la aplicación mejorada como se
describe en la sección Almacén en tiempo de ejecución (sin una referencia de tiempo de compilación).

Código de ejemplo
En el código de ejemplo (cómo descargar) se muestran escenarios de implementación de inicio de hospedaje:
Cada uno de los dos ensamblados de inicio de hospedaje (bibliotecas de clases) establece un par clave-valor
de configuración en memoria:
Paquete NuGet (HostingStartupPackage)
Biblioteca de clases (HostingStartupLibrary)
Se activa un inicio de hospedaje desde un ensamblado implementado por el almacén de tiempo de ejecución
(StartupDiagnostics). Este ensamblado agrega dos middleware a la aplicación (mientras se inicia) que
proporcionan información de diagnóstico en:
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:
Activación desde un paquete NuGet
1. Compile el paquete HostingStartupPackage con el comando dotnet pack.
2. Agregue el nombre de ensamblado del paquete de HostingStartupPackage a la variable de entorno
ASPNETCORE_HOSTINGSTARTUPASSEMBLIES .

3. Compile y ejecute la aplicación. Una referencia de paquete está presente en la aplicación mejorada (una
referencia de tiempo de compilación). El parámetro <PropertyGroup> del archivo del proyecto de la
aplicación especifica el resultado del proyecto de paquete (../HostingStartupPackage/bin/Debug) como
origen del paquete. Esto permite que la aplicación utilice el paquete sin cargar el paquete a nuget.org.
Para obtener más información, vea las notas que encontrará en el archivo del proyecto de
HostingStartupApp.

<PropertyGroup>

<RestoreSources>$(RestoreSources);https://api.nuget.org/v3/index.json;../HostingStartupPackage/bin/De
bug</RestoreSources>
</PropertyGroup>

4. Observe que los valores de la clave de configuración del servicio representados por la página de índice
coinciden con los valores establecidos por método ServiceKeyInjection.Configure del paquete.

Si realiza cambios en el proyecto HostingStartupPackage y lo vuelve a compilar, borre las cachés del paquete
NuGet local para asegurarse de que HostingStartupApp recibe el paquete actualizado y no un paquete en
desuso de la caché local. Para borrar las cachés de NuGet locales, ejecute el siguiente comando dotnet nuget
locals:

dotnet nuget locals all --clear

Activación desde una biblioteca de clases


1. Compile la biblioteca de clases HostingStartupLibrary con el comando dotnet build.
2. Agregue el nombre del ensamblado de la biblioteca de clases HostingStartupLibrary a la variable de
entorno ASPNETCORE_HOSTINGSTARTUPASSEMBLIES .
3. Implemente con bin-el ensamblado de la biblioteca de clases en la aplicación copiando el archivo
HostingStartupLibrary.dll desde el resultado compilado de la biblioteca de aplicaciones en la carpeta
bin/Debug de la aplicación.
4. Compile y ejecute la aplicación. Un parámetro <ItemGroup> del archivo del proyecto de la aplicación hace
referencia al ensamblado de la biblioteca de clases
(.\bin\Debug\netcoreapp2.1\HostingStartupLibrary.dll) (una referencia de tiempo de compilación). Para
obtener más información, vea las notas que encontrará en el archivo del proyecto de HostingStartupApp.

<ItemGroup>
<Reference Include=".\bin\Debug\netcoreapp2.1\HostingStartupLibrary.dll">
<HintPath>.\bin\Debug\netcoreapp2.1\HostingStartupLibrary.dll</HintPath>
<SpecificVersion>False</SpecificVersion>
</Reference>
</ItemGroup>

5. Observe que los valores de la clave de configuración del servicio representados por la página de índice
coinciden con los valores establecidos por el método ServiceKeyInjection.Configure de la biblioteca de
clases.
Activación desde un ensamblado implementado por el almacén de tiempo de ejecución
1. El proyecto StartupDiagnostics usa PowerShell para modificar el archivo StartupDiagnostics.deps.json.
PowerShell se instala de forma predeterminada en 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 StartupDiagnostics. Después de compilar el proyecto, un destino de compilación en
el archivo de proyecto realiza automáticamente las acciones siguientes:
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. Ejecute el comando dotnet store en un símbolo del sistema en el directorio de inicio de hospedaje para
almacenar el ensamblado y sus dependencias en el almacén de tiempo de ejecución del perfil de usuario:

dotnet store --manifest StartupDiagnostics.csproj --runtime <RID>

Para Windows, el comando usa el identificador en tiempo de ejecución (RID ) win7-x64 . Para
proporcionar el inicio de hospedaje para otro tiempo de ejecución, sustituya el RID correcto.
4. Establezca las variables de entorno:
Agregue el nombre de ensamblado de StartupDiagnostics a la variable de entorno
ASPNETCORE_HOSTINGSTARTUPASSEMBLIES .
En Windows, establezca la variable de entorno DOTNET_ADDITIONAL_DEPS en
%UserProfile%\.dotnet\x64\additionalDeps\StartupDiagnostics\ . En macOS o Linux, establezca la
variable de entorno DOTNET_ADDITIONAL_DEPS en
/Users/<USER>/.dotnet/x64/additionalDeps/StartupDiagnostics/ , donde <USER> es el perfil de usuario
que contiene el inicio de hospedaje.
5. Ejecute la aplicación de ejemplo.
6. 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.
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
12/09/2018 • 42 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


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.
Si una aplicación acepta únicamente solicitudes de una red interna, Kestrel se puede usar directamente
como servidor de la aplicación.

Si expone la aplicación en Internet, use IIS, Nginx o Apache 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.

Se necesita un proxy inverso para las implementaciones de borde (expuestas al tráfico de Internet) por
motivos de seguridad. Las versiones 1.x de Kestrel no tienen un complemento completo de defensas frente
a los ataques, como unos tiempos de espera, unos límites de tamaño y unos límites de conexiones
simultáneas adecuados.
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


El paquete Microsoft.AspNetCore.Server.Kestrel se incluye en el metapaquete Microsoft.AspNetCore.App
(ASP.NET Core 2.1 o posterior).
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)


{
CreateWebHostBuilder(args).Build().Run();
}

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>


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

Para proporcionar configuración adicional después de llamar a CreateDefaultBuilder , use


ConfigureKestrel :

.ConfigureKestrel((context, options) =>


{
// Set properties and call methods on options
});

Para proporcionar configuración adicional después de llamar a CreateDefaultBuilder , llame a UseKestrel:


.UseKestrel(options =>
{
// Set properties and call methods on options
});

Instale el paquete NuGet Microsoft.AspNetCore.Server.Kestrel.


Llame al método de extensión UseKestrel en WebHostBuilder en el método Main , y especifique cualquier
opción de Kestrel que necesite, como se muestra en la siguiente sección.

public static void Main(string[] args)


{
Console.WriteLine("Running demo with Kestrel.");

var config = new ConfigurationBuilder()


.AddCommandLine(args)
.Build();

var builder = new WebHostBuilder()


.UseContentRoot(Directory.GetCurrentDirectory())
.UseConfiguration(config)
.UseStartup<Startup>()
.UseKestrel(options =>
{
if (config["threadCount"] != null)
{
options.ThreadCount = int.Parse(config["threadCount"]);
}
})
.UseUrls("http://localhost:5000");

var host = builder.Build();


host.Run();
}

Opciones de Kestrel
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:

.ConfigureKestrel((context, options) =>


{
options.Limits.MaxConcurrentConnections = 100;
});
.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 .

.ConfigureKestrel((context, options) =>


{
options.Limits.MaxConcurrentUpgradedConnections = 100;
});

.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:
.ConfigureKestrel((context, options) =>
{
options.Limits.MaxRequestBodySize = 10 * 1024;
});

.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:
.ConfigureKestrel((context, options) =>
{
options.Limits.MinRequestBodyDataRate =
new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(10));
options.Limits.MinResponseDataRate =
new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(10));
});

.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
Para más información sobre las opciones y límites de Kestrel, vea:
KestrelServerOptions (clase)
KestrelServerLimits
Configuración de punto de conexión
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.
devuelve un
options.Configure(context.Configuration.GetSection("Kestrel"))
KestrelConfigurationLoadercon un método .Endpoint(string name, options => { }) que se puede
usar para complementar la configuración de un punto de conexión configurado:

options.Configure(context.Configuration.GetSection("Kestrel"))
.Endpoint("HTTPS", opt =>
{
opt.HttpsOptions.SslProtocols = SslProtocols.Tls12;
});

También puede acceder directamente a KestrelServerOptions.ConfigurationLoader para proseguir


con la iteración en el cargador existente, como la proporcionada por WebHost.CreateDefaultBuilder.
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


options.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 lo siguiente:
Ejecutarse en el marco 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.
Todos los sitios web deben ejecutarse en la misma instancia de Kestrel. Kestrel no admite el uso
compartido de una dirección IP y un puerto entre varias instancias sin un proxy inverso.
WebHost.CreateDefaultBuilder()
.ConfigureKestrel((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<string, X509Certificate2>(
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;
};
});
});
});
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<string, X509Certificate2>(
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)


{
CreateWebHostBuilder(args).Build().Run();
}

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>


WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.ConfigureKestrel((context, options) =>
{
options.Listen(IPAddress.Loopback, 8000);
options.Listen(IPAddress.Loopback, 8001, listenOptions =>
{
listenOptions.UseHttps("testCert.pfx", "testPassword");
});
});
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}

public static IWebHostBuilder CreateWebHostBuilder(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");
});
});

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:

.ConfigureKestrel((context, options) =>


{
options.ListenUnixSocket("/tmp/kestrel-test.sock");
options.ListenUnixSocket("/tmp/kestrel-test.sock", listenOptions =>
{
listenOptions.UseHttps("testCert.pfx", "testpassword");
});
});

.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:
public void Configure(IApplicationBuilder app)
{
var serverAddressesFeature =
app.ServerFeatures.Get<IServerAddressesFeature>();

app.UseStaticFiles();

app.Run(async (context) =>


{
context.Response.ContentType = "text/html";
await context.Response
.WriteAsync("<!DOCTYPE html><html lang=\"en\"><head>" +
"<title></title></head><body><p>Hosted by Kestrel</p>");

if (serverAddressesFeature != null)
{
await context.Response
.WriteAsync("<p>Listening on the following addresses: " +
string.Join(", ", serverAddressesFeature.Addresses) +
"</p>");
}

await context.Response.WriteAsync("<p>Request URL: " +


$"{context.Request.GetDisplayUrl()}<p>");
});
}

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:

Listening on the following addresses: 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.
ASP.NET Core se enlaza a http://localhost:5000 de forma predeterminada. Configure los puertos y los
prefijos de dirección URL para Kernel usando lo siguiente:
El método de extensión UseUrls
El argumento de la línea de comandos --urls
La clave de configuración de host urls
El sistema de configuración de ASP.NET Core, incluida la variable de entorno ASPNETCORE_URLS

Para más información sobre estos métodos, vea Hospedaje.


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 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 metapackage 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.
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.
Dirección IPv4 con número de puerto

http://65.55.39.10:80/
https://65.55.39.10:443/

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/
https://[0:0:0:0:0:ffff:4137:270a]:443/

[::] 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/
https://contoso.com:443/
https://*:443/

Los nombres de host * y + no son especiales. Todo lo que no sea una dirección IP o un localhost
reconocido 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 WebListener o un servidor
proxy inverso, como IIS, Nginx o Apache.
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.
Socket de UNIX

http://unix:/run/dan-live.sock

Puerto 0
Cuando se especifica el número de puerto 0 , Kestrel se enlaza de forma dinámica a un puerto disponible.
El enlace al puerto 0 está permitido en cualquier nombre de host o IP, excepto en localhost .
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

Prefijos de URL para SSL


Si llama al método de extensión UseHttps , procure incluir los prefijos de URL con https: :

var host = new WebHostBuilder()


.UseKestrel(options =>
{
options.UseHttps("testCert.pfx", "testPassword");
})
.UseUrls("http://localhost:5000", "https://localhost:5001")
.UseContentRoot(Directory.GetCurrentDirectory())
.UseStartup<Startup>()
.Build();

NOTE
HTTP y HTTPS no se pueden hospedar en el mismo puerto.
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.

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.
Como solución alternativa, hospede detrás de un proxy inverso con filtrado de encabezados de host. Este es
el único escenario admitido de Kestrel en ASP.NET Core 1.x.
Como solución alternativa, use un middleware para filtrar las solicitudes por el encabezado Host :

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);
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 espera una clave AllowedHosts en appsettings.json/appsettings.<EnvironmentName>.json.


El valor es una lista delimitada por punto y coma de nombres de host sin los números de puerto:
Como solución alternativa, use el Middleware de filtrado de hosts. El Middleware de filtrado de hosts se
facilita a través del paquete Microsoft.AspNetCore.HostFiltering, que se incluye en el metapaquete
Microsoft.AspNetCore.App (ASP.NET Core 2.1 o posterior). Este middleware se agrega por medio de
CreateDefaultBuilder, que llama a AddHostFiltering:

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 Middleware de filtrado de hosts está deshabilitado de forma predeterminada. Para habilitarlo, defina una
clave AllowedHosts en appsettings.json/appsettings.<EnvironmentName>.json. El valor es una lista
delimitada por punto y coma de nombres de host sin los números de puerto:
appsettings.json:

{
"AllowedHosts": "example.com;localhost"
}
NOTE
El Middleware de encabezados reenviados también tiene una opción ForwardedHeadersOptions.AllowedHosts. El
Middleware de encabezados reenviados y el Middleware de filtrado de hosts tienen una funcionalidad similar en
diferentes escenarios. Establecer AllowedHosts con el Middleware de encabezados reenviados es adecuado cuando
el encabezado de host no se conserva mientras se reenvían solicitudes con un servidor proxy inverso o un
equilibrador de carga. Establecer AllowedHosts con el Middleware de filtrado de hosts es adecuado cuando se usa
Kestrel como un servidor perimetral, o cuando el encabezado de host se reenvía directamente.
Para más información sobre el Middleware de encabezados reenviados, vea Configurar ASP.NET Core para trabajar
con servidores proxy y equilibradores de carga.

Recursos adicionales
Aplicación de HTTPS
Código fuente de Kestrel
RFC 7230: Message Syntax and Routing (Section 5.4: Host) (RFC 7230: Enrutamiento y sintaxis de
mensajes [Sección 5.4: Host])
Configurar ASP.NET Core para trabajar con servidores proxy y equilibradores de carga
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
31/08/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.

Autenticación de modo kernel con Kerberos


HTTP.sys delega en la autenticación de modo kernel con el protocolo de autenticación de Kerberos. La
autenticación de modo usuario no se admite con Kerberos y HTTP.sys. Se debe usar la cuenta de equipo
para descifrar el token o el vale de Kerberos que se obtiene de Active Directory y que el cliente reenvía al
servidor para autenticar al usuario. Registre el nombre de entidad de seguridad de servicio (SPN ) para el
host, no el usuario de la aplicación.

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.App (nuget.org) (ASP.NET Core 2.1 o posterior). Si no usa el metapaquete
Microsoft.AspNetCore.App , 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

PROPIEDAD. DESCRIPCIÓN 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


PROPIEDAD. DESCRIPCIÓN DEFAULT

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 .

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.
PROPIEDAD. DESCRIPCIÓN 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:
TimeoutManager.DrainEntit
yBody – Tiempo permitido
para que la API HTTP
Server purgue el cuerpo de
la entidad en una conexión
persistente.
TimeoutManager.EntityBod
y – Tiempo permitido para
que llegue el cuerpo de la
entidad de solicitud.
TimeoutManager.HeaderW
ait – Tiempo permitido para
que la API HTTP Server
analice el encabezado de
solicitud.
TimeoutManager.IdleConne
ction – Tiempo permitido
para una conexión inactiva.
TimeoutManager.MinSendB
ytesPerSecond – Tasa
mínima de envío de la
respuesta.
TimeoutManager.RequestQ
ueue – 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
Estado de sesión y aplicación en ASP.NET Core
27/08/2018 • 34 minutes to read • Edit Online

Por Rick Anderson, Steve Smith, Diana LaRose y Luke Latham


HTTP es un protocolo sin estado. Sin realizar pasos adicionales, las solicitudes HTTP son mensajes
independientes que no conservan los valores de usuario ni el estado de la aplicación. En este artículo se
describen varios enfoques para conservar el estado de la aplicación y los datos de usuario entre las solicitudes.
Vea o descargue el código de ejemplo (cómo descargarlo)

Administración de estado
El estado se puede almacenar mediante varios enfoques. Cada enfoque se describe más adelante en este tema.

ENFOQUE DE ALMACENAMIENTO MECANISMO DE ALMACENAMIENTO

Cookies Cookies HTTP (pueden incluir los datos almacenados


mediante el código de aplicación del lado servidor)

Estado de sesión Cookies HTTP y código de aplicación del lado servidor

TempData Cookies HTTP o estado de sesión

Cadenas de consulta Cadenas de consulta HTTP

Campos ocultos Campos de formularios HTTP

HttpContext.Items Código de aplicación del lado servidor

Caché Código de aplicación del lado servidor

Inserción de dependencias Código de aplicación del lado servidor

Cookies
Las cookies almacenan datos de todas las solicitudes. 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 con los datos
almacenados por la aplicación. La mayoría de los exploradores restringen el tamaño de las cookies a 4096 bytes.
Solo hay disponible un número limitado de cookies para cada dominio.
Como las cookies están expuestas a alteraciones, deben validarse por la aplicación. Los usuarios pueden eliminar
las cookies y estas pueden caducar en los clientes. Pero las cookies 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. En la mayoría de los
casos, el usuario solo se identifica y no se autentica. La cookie puede almacenar el nombre de usuario, el nombre
de cuenta o el id. de usuario único (por ejemplo, un GUID ). Después, puede usar la cookie para tener acceso a la
configuración personalizada del usuario, como su color de fondo del sitio web preferido.
Preste atención al reglamento general de protección de datos (GDPR ) de la Unión Europea cuando emita cookies
y trate con casos de privacidad. Para obtener más información, vea Compatibilidad con el Reglamento general de
protección de datos (RGPD ) en ASP.NET Core.

Estado de sesión
El estado de sesión es un escenario de ASP.NET Core para el almacenamiento de datos de usuario mientras el
usuario examina una aplicación web. El estado de sesión usa un almacén mantenido por la aplicación para
conservar los datos en las solicitudes de un cliente. Los datos de sesión están respaldados por una memoria
caché y se consideran datos efímeros y el sitio debería continuar funcionando correctamente sin los datos de
sesión.

NOTE
La sesión no es compatible con aplicaciones SignalR porque un concentrador SignalR podría ejecutarse independientemente
de un contexto HTTP. Por ejemplo, esto puede ocurrir cuando un concentrador mantiene abierta una solicitud de sondeo
larga más allá de la duración del contexto HTTP de la solicitud.

Para mantener el estado de sesión, ASP.NET Core proporciona una cookie al cliente que contiene un
identificador de sesión, que se envía a la aplicación con cada solicitud. La aplicación usa el identificador de sesión
para capturar los datos de sesión.
El estado de sesión muestra los siguientes comportamientos:
Dado que la cookie de sesión es específica del explorador, las sesiones no se comparten 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.
Las sesiones vacías no se mantienen y la sesión debe tener, al menos un conjunto de valores en ella para que
se conserve entre solicitudes. Cuando una sesión no se conserva, se genera un nuevo identificador de sesión
para cada nueva solicitud.
La aplicación conserva una sesión durante un tiempo limitado después de la última solicitud. La aplicación
especifica un tiempo de espera de sesión o usa 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 entre las sesiones.
Los datos de sesión se eliminan cuando se llama a la implementación ISession.Clear o cuando expira la
sesión.
No hay ningún mecanismo predeterminado para informar al código de aplicación que se ha cerrado un
explorador del cliente o cuando la cookie de sesión se elimina o caduca en el cliente.

WARNING
No almacene datos confidenciales en un estado de sesión. El usuario podría no cerrar el explorador y borrar la cookie de
sesión. Algunos exploradores mantienen las cookies de sesión válidas en las ventanas del explorador. Es posible que una
sesión no esté restringida a un único usuario y que el siguiente usuario continúe examinando la aplicación con la misma
cookie de sesión.

El proveedor de caché en memoria almacena datos de sesión en la memoria del servidor donde reside la
aplicación. En un escenario de granja de servidores:
Use sesiones permanentes para asociar cada sesión a una instancia de aplicación específica en un servidor
individual. Azure App Service usa enrutamiento de solicitud de aplicaciones (ARR ) para exigir sesiones
permanentes de forma predeterminada. Pero las sesiones permanentes pueden afectar a la escalabilidad y
complicar la actualización de las aplicaciones web. Un enfoque mejor consiste en usar una memoria caché
distribuida de Redis o SQL Server, que no requiere sesiones permanentes. Para obtener más información, vea
Working with a Distributed Cache (Trabajar con una memoria caché distribuida).
La cookie de sesión se cifra mediante IDataProtector. La protección de datos debe configurarse correctamente
para que lea las cookies de sesión en cada equipo. Para obtener más información, vea Protección de datos en
ASP.NET Core y Key storage providers (Proveedores de almacenamiento de claves).
Configurar el estado de sesión
El paquete Microsoft.AspNetCore.Session, que se incluye en el metapaquete Microsoft.AspNetCore.App,
proporciona middleware para administrar el estado de sesión. Para habilitar el middleware de sesión, Startup
debe contener:
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. Para obtener más información, vea Working with a Distributed
Cache (Trabajar con una memoria caché distribuida).
Una llamada a AddSession en ConfigureServices .
Una llamada a UseSession en Configure .
El código siguiente muestra cómo configurar el proveedor de sesión en memoria con una implementación en
memoria de IDistributedCache :
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});

services.AddDistributedMemoryCache();

services.AddSession(options =>
{
// Set a short timeout for easy testing.
options.IdleTimeout = TimeSpan.FromSeconds(10);
options.Cookie.HttpOnly = true;
});

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.UseSession();
app.UseHttpContextItemsMiddleware();
app.UseMvc();
}
}
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddDistributedMemoryCache();

services.AddSession(options =>
{
// Set a short timeout for easy testing.
options.IdleTimeout = TimeSpan.FromSeconds(10);
options.CookieHttpOnly = true;
});

services.AddMvc();
}

public void Configure(IApplicationBuilder app)


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

El orden del middleware es importante. En el ejemplo anterior, se produce una excepción


InvalidOperationException cuando UseSession se invoca después de UseMvc . Para obtener más información vea
Ordenación de Middleware.
HttpContext.Session está disponible después de configurar el estado de sesión.
No se puede acceder a HttpContext.Session antes de llamar a UseSession .
No se puede crear una nueva sesión con una cookie de sesión nueva después de que la aplicación haya
empezado a escribir en la secuencia de respuesta. La excepción se registra en el registro del servidor web y no se
muestra en el explorador.
Cargar de forma asincrónica el estado de sesión
El proveedor de sesión predeterminado de ASP.NET Core carga asincrónicamente los registros de sesión desde
la memoria auxiliar subyacente IDistributedCache solo si se llama explícitamente al método ISession.LoadAsync
antes que a 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 conllevar una disminución del rendimiento al escalar.
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.

Opciones de sesión
Para reemplazar los valores predeterminados de la sesión, use SessionOptions.

OPCIÓN DESCRIPCIÓN
OPCIÓN DESCRIPCIÓN

Cookie Determina la configuración usada para crear la cookie. Name


tiene como valor predeterminado
SessionDefaults.CookieName ( .AspNetCore.Session ). Path
tiene como valor predeterminado SessionDefaults.CookiePath
( / ). SameSite tiene como valor predeterminado
SameSiteMode.Lax ( 1 ). HttpOnly tiene como valor
predeterminado true . IsEssential tiene como valor
predeterminado false .

IdleTimeout IdleTimeout indica cuánto tiempo puede estar inactiva la


sesión antes de que se abandone su contenido. Cada acceso
a la sesión restablece el tiempo de espera. Tenga en cuenta
que esto solo es aplicable al contenido de la sesión, no a la
cookie. El valor predeterminado es de 20 minutos.

IOTimeout El período máximo de tiempo permitido para cargar una


sesión del almacén o devolverla a él. Tenga en cuenta que
esto solo es aplicable a las operaciones asincrónicas. Puede
deshabilitar este tiempo de espera mediante
InfiniteTimeSpan. El valor predeterminado es 1 minuto.

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 .AspNetCore.Session y usa 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 HttpOnly tiene como valor predeterminado true ).

OPCIÓN DESCRIPCIÓN

CookieDomain Determina el dominio usado para crear la cookie.


CookieDomain no se encuentra configurado de forma
predeterminada.

CookieHttpOnly Determina si el explorador debe permitir que la cookie tenga


acceso a código JavaScript del lado cliente. El valor
predeterminado es true , lo que significa que la cookie solo
se pasa a las solicitudes HTTP y no está disponible para script
en la página.

CookieName Determina el nombre de cookie que se usa para conservar el


identificador de sesión. El valor predeterminado es
SessionDefaults.CookieName ( .AspNetCore.Session ).

CookiePath Determina la ruta de acceso usada para crear la cookie. Tiene


como valor predeterminado SessionDefaults.CookiePath ( / ).

CookieSecure Determina si la cookie solo se debe transmitir en las


solicitudes HTTPS. El valor predeterminado es
CookieSecurePolicy.None ( 2 ).

IdleTimeout IdleTimeout indica cuánto tiempo puede estar inactiva la


sesión antes de que se abandone su contenido. Cada acceso
a la sesión restablece el tiempo de espera. Tenga en cuenta
que esto solo es aplicable al contenido de la sesión, no a la
cookie. El valor predeterminado es de 20 minutos.
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 usa una ruta de acceso de
/ .

Para reemplazar los valores predeterminados de la sesión de cookies, use SessionOptions :

public void ConfigureServices(IServiceCollection services)


{
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});

services.AddDistributedMemoryCache();

services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

services.AddSession(options =>
{
options.Cookie.Name = ".AdventureWorks.Session";
options.IdleTimeout = TimeSpan.FromSeconds(10);
});
}

public void ConfigureServices(IServiceCollection services)


{
services.AddDistributedMemoryCache();

services.AddSession(options =>
{
options.CookieName = ".AdventureWorks.Session";
options.IdleTimeout = TimeSpan.FromSeconds(10);
});

services.AddMvc();
}

La aplicación usa la propiedad IdleTimeout para determinar el tiempo que una sesión puede estar inactiva antes
de que se abandone el contenido de la caché de servidor. Esta propiedad es independiente de la expiración de la
cookie. Cada solicitud que se pasa a través del middleware de sesión restablece el tiempo de espera.
El estado de sesión es no realiza bloqueo. Si dos solicitudes intentan modificar el contenido de una sesión
simultáneamente, la última solicitud reemplaza a la primera. Session se implementa como una sesión coherente,
lo que significa que todo el contenido se almacena junto. Cuando dos solicitudes buscan modificar diferentes
valores de sesión, la última solicitud podría reemplazar los cambios de sesión realizados por la primera.
Establecer y obtener valores de Session
Se accede al estado de sesión desde una clase PageModel de Razor Pages o una clase Controller de MVC con
HttpContext.Session. Esta propiedad es una implementación de ISession.
La implementación ISession proporciona varios métodos de extensión para establecer y recuperar valores de
cadena y enteros. Los métodos de extensión están en el espacio de nombres Microsoft.AspNetCore.Http
(agregue una instrucción using Microsoft.AspNetCore.Http; para obtener acceso a los métodos de extensión)
cuando el proyecto hace referencia al paquete Microsoft.AspNetCore.Http.Extensions. Ambos paquetes están
incluidos en el metapaquete Microsoft.AspNetCore.App.
La implementación ISession proporciona varios métodos de extensión para establecer y recuperar valores de
cadena y enteros. Los métodos de extensión están en el espacio de nombres Microsoft.AspNetCore.Http
(agregue una instrucción using Microsoft.AspNetCore.Http; para obtener acceso a los métodos de extensión)
cuando el proyecto hace referencia al paquete Microsoft.AspNetCore.Http.Extensions.
Métodos de extensión ISession :
Get(ISession, String)
GetInt32(ISession, String)
GetString(ISession, String)
SetInt32(ISession, String, Int32)
SetString(ISession, String, String)
En el ejemplo siguiente se recupera el valor de sesión para la clave IndexModel.SessionKeyName ( _Name en la
aplicación de ejemplo) en una página de Razor Pages:

@page
@using Microsoft.AspNetCore.Http
@model IndexModel

...

Name: @HttpContext.Session.GetString(IndexModel.SessionKeyName)

En el ejemplo siguiente se muestra cómo establecer y obtener un entero y una cadena:

public class IndexModel : PageModel


{
public const string SessionKeyName = "_Name";
public const string SessionKeyAge = "_Age";
const string SessionKeyTime = "_Time";

public string SessionInfo_Name { get; private set; }


public string SessionInfo_Age { get; private set; }
public string SessionInfo_CurrentTime { get; private set; }
public string SessionInfo_SessionTime { get; private set; }
public string SessionInfo_MiddlewareValue { get; private set; }

public void OnGet()


{
// Requires: using Microsoft.AspNetCore.Http;
if (string.IsNullOrEmpty(HttpContext.Session.GetString(SessionKeyName)))
{
HttpContext.Session.SetString(SessionKeyName, "The Doctor");
HttpContext.Session.SetInt32(SessionKeyAge, 773);
}

var name = HttpContext.Session.GetString(SessionKeyName);


var age = HttpContext.Session.GetInt32(SessionKeyAge);
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}\"");


}

Todos los datos de sesión se deben serializar para habilitar un escenario de caché distribuida, incluso cuando se
usa la caché en memoria. Se proporcionan una cadena mínima y serializadores de número (vea los métodos y los
métodos de extensión de ISession). El usuario debe serializar los tipos complejos mediante otro mecanismo,
como JSON.
Agregue los siguientes métodos de extensión, para establecer y obtener objetos serializables:

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

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 con los método de
extensión:

// Requires you add the Set and Get extension method mentioned in the topic.
if (HttpContext.Session.Get<DateTime>(SessionKeyTime) == default(DateTime))
{
HttpContext.Session.Set<DateTime>(SessionKeyTime, currentTime);
}

public IActionResult SetDate()


{
// Requires you add the Set extension method mentioned in the topic.
HttpContext.Session.Set<DateTime>(SessionKeyDate, DateTime.Now);

return RedirectToAction("GetDate");
}

public IActionResult GetDate()


{
// Requires you add the Get extension method mentioned in the topic.
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}");
}

TempData
ASP.NET Core expone la propiedad TempData de un modelo de página de Razor Pages o TempData de un
controlador de MVC. 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 para más de una única solicitud. Los proveedores de TempData implementan
TempData mediante cookies o estado de sesión.
Proveedores de TempData
En ASP.NET Core 2.0 y versiones posteriores, el proveedor TempData basado en cookies se usa 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.
En ASP.NET Core 1.0 y 1.1, el proveedor TempData de estado de sesión es el proveedor predeterminado.
Elegir un proveedor TempData
Elegir un proveedor TempData implica una serie de consideraciones:
1. ¿La aplicación ya usa el estado de sesión? Si es así, el uso 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 agrega 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 en varios servidores? Si es así, no es necesaria ninguna
configuración adicional para usar el proveedor de TempData de cookies además de la protección de datos
(vea Protección de datos y Key storage providers (Proveedores de almacenamiento de claves)).

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. Cuando use el proveedor TempData de cookies, compruebe que la aplicación no
supera esos límites. Tenga en cuenta el tamaño total de los datos. Cuenta para los aumentos de tamaño de cookie debidos
a la fragmentación y el cifrado.

Configurar el proveedor TempData


El proveedor TempData basado en cookies está habilitado de forma predeterminada.
Para habilitar el proveedor TempData basado en sesión, use el método de extensión
AddSessionStateTempDataProvider:

public void ConfigureServices(IServiceCollection services)


{
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});

services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
.AddSessionStateTempDataProvider();

services.AddSession();
}

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.UseSession();
app.UseMvc();
}

El siguiente código de clase Startup configura el proveedor TempData basado en sesión:


public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddSession();
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)


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

El orden del middleware es importante. En el ejemplo anterior, se produce una excepción


InvalidOperationException cuando UseSession se invoca después de UseMvc . Para obtener más información vea
Ordenación de Middleware.

IMPORTANT
Si el destino es .NET Framework y usa el proveedor TempData basado en sesión, agregue el paquete
Microsoft.AspNetCore.Session al proyecto.

Cadenas de consulta
Se 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. Dado que las cadenas de
consulta de direcciones URL son públicas, nunca use las cadenas de consulta para datos confidenciales.
Además del uso compartido no intencionado, la inclusión de datos en las 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. Después, 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, vea Preventing Cross-
Site Request Forgery (XSRF/CSRF ) Attacks (Evitar los ataques de falsificación de solicitud entre sitios
[XSRF/CSRF ]).

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. Dado que el cliente puede llegar a alterar los datos, la aplicación
siempre debe revalidar los datos almacenados en campos ocultos.

HttpContext.Items
La colección HttpContext.Items se usa para almacenar los datos al procesar una única solicitud. El contenido de la
colección se descarta después de procesar una solicitud. A menudo se usa la colección Items para permitir que
los componentes o el middleware se comuniquen cuando operan en distintos puntos en el tiempo durante una
solicitud y no pueden pasarse parámetros de forma directa.
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 puede acceder al valor de isVerified :

app.Run(async (context) =>


{
await context.Response.WriteAsync($"Verified: {context.Items["isVerified"]}");
});

Si el middleware solo se usa en una única aplicación, se aceptan claves string . El middleware compartido entre
instancias de aplicación debería usar claves de objeto únicas para evitar conflictos de clave. En el ejemplo
siguiente se muestra cómo usar una clave de objeto única definida en una clase de middleware:

public class HttpContextItemsMiddleware


{
private readonly RequestDelegate _next;
public static readonly object HttpContextItemsMiddlewareKey = new Object();

public HttpContextItemsMiddleware(RequestDelegate next)


{
_next = next;
}

public async Task Invoke(HttpContext httpContext)


{
httpContext.Items[HttpContextItemsMiddlewareKey] = "K-9";

await _next(httpContext);
}
}

public static class HttpContextItemsMiddlewareExtensions


{
public static IApplicationBuilder
UseHttpContextItemsMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<HttpContextItemsMiddleware>();
}
}
// You may need to install the Microsoft.AspNetCore.Http.Abstractions package
public class HttpContextItemsMiddleware
{
private readonly RequestDelegate _next;
public static readonly object HttpContextItemsMiddlewareKey = new Object();

public HttpContextItemsMiddleware(RequestDelegate next)


{
_next = next;
}

public async Task Invoke(HttpContext httpContext)


{
httpContext.Items[HttpContextItemsMiddlewareKey] = "K-9";

await _next(httpContext);
}
}

public static class HttpContextItemsMiddlewareExtensions


{
public static IApplicationBuilder
UseHttpContextItemsMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<HttpContextItemsMiddleware>();
}
}

Otro código puede tener acceso al valor almacenado en HttpContext.Items con la clave que expone la clase de
middleware:

HttpContext.Items
.TryGetValue(HttpContextItemsMiddleware.HttpContextItemsMiddlewareKey,
out var middlewareSetValue);
SessionInfo_MiddlewareValue =
middlewareSetValue?.ToString() ?? "Middleware value not set!";

public IActionResult GetMiddlewareValue()


{
var middlewareSetValue =
HttpContext.Items[HttpContextItemsMiddleware.HttpContextItemsMiddlewareKey];

return Content($"Value: {middlewareSetValue}");


}

Este enfoque también tiene la ventaja de eliminar el uso de cadenas de claves en el código.

instancias y claves
El almacenamiento en caché es una manera eficaz de almacenar y recuperar datos. La aplicación puede controlar
la duración de los elementos almacenados en caché.
Los datos almacenados en caché no están asociados a una solicitud, usuario o sesión específicos. Procure no
almacenar en caché datos específicos de usuario que podrían recuperar las solicitudes de otros
usuarios.
Para obtener más información, vea el tema Almacenamiento en caché de respuestas.

Inserción de dependencias
Use inserción de dependencias para que los datos estén disponibles para todos los usuarios:
1. Definir un servicio que contiene los datos. Por ejemplo, una clase denominada MyAppData se define:

public class MyAppData


{
// Declare properties and methods
}

2. Agregue la clase de servicio a Startup.ConfigureServices :

public void ConfigureServices(IServiceCollection services)


{
services.AddSingleton<MyAppData>();
}

3. Use la clase de servicio de datos:

public class IndexModel : PageModel


{
public IndexModel(MyAppData myService)
{
// Do something with the service
// Examples: Read data, store in a field or property
}
}

public class HomeController : Controller


{
public HomeController(MyAppData myService)
{
// Do something with the service
// Examples: Read data, store in a field or property
}
}

Errores comunes
"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
obtener 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 memoria auxiliar
no está disponible), el middleware registra la excepción y la solicitud continúa con normalidad. Esto
provoca un comportamiento imprevisible.
Por ejemplo, un usuario almacena un carro de la compra en la sesión. El usuario agrega un elemento al
carro, pero se produce un error en la confirmación. La aplicación no se percata del error y notifica al
usuario que el elemento se ha agregado al carro, lo cual no es cierto.
El enfoque recomendado para comprobar los errores es llamar a await feature.Session.CommitAsync();
desde el código de la aplicación cuando esta haya terminado de escribir en la sesión. CommitAsync produce
una excepción si la memoria auxiliar no está disponible. Si CommitAsync produce un error, la aplicación
puede procesar la excepción. LoadAsync se produce en las mismas condiciones donde el almacén de datos
no está disponible.

Recursos adicionales
Hospedaje de ASP.NET Core en una granja de servidores web
Proveedores de archivo en ASP.NET Core
09/08/2018 • 14 minutes to read • Edit Online

Por Steve Smith y Luke Latham


ASP.NET Core abstrae el acceso al sistema de archivos mediante el uso de proveedores de archivos. Los
proveedores de archivos se usan en el marco de ASP.NET Core:
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 usa proveedores de archivos para localizar páginas y vistas.
Las herramientas de .NET Core usan proveedores de archivos y patrones globales para especificar los archivos
que deben publicarse.
Vea o descargue el código de ejemplo (cómo descargarlo)

Interfaces de proveedor de archivos


La interfaz principal es IFileProvider. IFileProvider expone métodos para:
Obtener información de archivo (IFileInfo).
Obtener información de directorio (IDirectoryContents).
Configurar las notificaciones de cambio (mediante IChangeToken).
IFileInfo proporciona métodos y propiedades para trabajar con archivos:
Exists
IsDirectory
Name
Length (en bytes)
Fecha LastModified
Puede leer del archivo mediante el método IFileInfo.CreateReadStream.
La aplicación de ejemplo muestra cómo configurar un proveedor de archivos en Startup.ConfigureServices para
su uso en toda la aplicación a través de la inserción de dependencias.

Implementaciones del proveedor de archivos


Hay tres implementaciones de IFileProvider disponibles.

IMPLEMENTACIÓN DESCRIPCIÓN

PhysicalFileProvider El proveedor físico se utiliza para acceder a los archivos físicos


del sistema.

ManifestEmbeddedFileProvider El proveedor insertado de manifiestos se utiliza para tener


acceder a archivos insertados en ensamblados.
IMPLEMENTACIÓN DESCRIPCIÓN

CompositeFileProvider El proveedor compuesto se utiliza para proporcionar acceso


combinado a archivos y directorios de uno o más
proveedores.

IMPLEMENTACIÓN DESCRIPCIÓN

PhysicalFileProvider El proveedor físico se utiliza para acceder a los archivos físicos


del sistema.

EmbeddedFileProvider El proveedor insertado se utiliza para tener acceso a archivos


insertados en ensamblados.

CompositeFileProvider 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. PhysicalFileProvider usa el tipo
System.IO.File (para el proveedor físico) y define el ámbito de todas las rutas de acceso a un directorio y sus
elementos secundarios. Esta definición de ámbito impide el acceso al sistema de archivos fuera del directorio
especificado y sus elementos secundarios. Al crear una instancia de este proveedor, se requiere una ruta de acceso
del directorio, que actúa como la ruta de acceso base para todas las solicitudes realizadas usando el proveedor.
Puede crear instancias de un proveedor PhysicalFileProvider directamente o puede solicitar IFileProvider en
un constructor a través de inserción de dependencias.
Tipos estáticos
El código siguiente muestra cómo crear PhysicalFileProvider y usarlo para obtener el contenido del directorio y
la información del archivo:

var provider = new PhysicalFileProvider(applicationRoot);


var contents = provider.GetDirectoryContents(string.Empty);
var fileInfo = provider.GetFileInfo("wwwroot/js/site.js");

Tipos del ejemplo anterior:


provider es IFileProvider .
contents es IDirectoryContents .
fileInfo es IFileInfo .
El proveedor de archivos puede usarse para iterar por el directorio especificado por applicationRoot o llamar a
GetFileInfo para obtener información de un archivo. El proveedor de archivos no tiene acceso fuera del
directorio applicationRoot .
La aplicación de ejemplo crea el proveedor en la clase Startup.ConfigureServices de la aplicación mediante
IHostingEnvironment.ContentRootFileProvider:

var physicalProvider = _env.ContentRootFileProvider;

Obtención de tipos de proveedor de archivos con inserción de dependencias


Inserte el proveedor en cualquier constructor de clase y asígnelo a un campo local. Utilice el campo en todos los
métodos de la clase para acceder a archivos.
En la aplicación de ejemplo, la clase IndexModel recibe una instancia IFileProvider para obtener el contenido del
directorio para la ruta de acceso base de la aplicación.
Páginas/Index.cshtml.cs:

public class IndexModel : PageModel


{
private readonly IFileProvider _fileProvider;

public IndexModel(IFileProvider fileProvider)


{
_fileProvider = fileProvider;
}

public IDirectoryContents DirectoryContents { get; private set; }

public void OnGet()


{
DirectoryContents = _fileProvider.GetDirectoryContents(string.Empty);
}
}

IDirectoryContents se iteran en la página.


Páginas/Index.cshtml:

<ul>
@foreach (var item in Model.DirectoryContents)
{
if (item.IsDirectory)
{
<li><strong>@item.Name</strong></li>
}
else
{
<li>@item.Name - @item.Length bytes</li>
}
}
</ul>

En la aplicación de ejemplo, la clase HomeController recibe una instancia IFileProvider para obtener el contenido
del directorio para la ruta de acceso base de la aplicación.
Controladores/HomeController.cs:
public class HomeController : Controller
{
private readonly IFileProvider _fileProvider;

public HomeController(IFileProvider fileProvider)


{
_fileProvider = fileProvider;
}

public IActionResult Index()


{
var contents = _fileProvider.GetDirectoryContents(string.Empty);

return View(contents);
}
}

IDirectoryContents se iteran en la vista.


Vistas/Inicio/Index.cshtml:

<ul>
@foreach (IFileInfo item in Model)
{
if (item.IsDirectory)
{
<li><strong>@item.Name</strong></li>
}
else
{
<li>@item.Name - @item.Length bytes</li>
}
}
</ul>

ManifestEmbeddedFileProvider
ManifestEmbeddedFileProvider se utiliza para acceder a archivos incrustados dentro de ensamblados.
ManifestEmbeddedFileProvider utiliza un manifiesto compilado en el ensamblado para reconstruir las rutas de
acceso originales de los archivos incrustados.

NOTE
ManifestEmbeddedFileProvider está disponible en ASP.NET Core 2.1 o versiones posteriores. Para acceder a archivos
incrustados en ensamblados en ASP.NET Core 2.0 o versiones anteriores, consulte el versión 1.x de ASP.NET Core de este
tema.

Para generar un manifiesto de los archivos incrustados, establezca la propiedad <GenerateEmbeddedFilesManifest>


en true . Especifique los archivos que desea incrustar con <EmbeddedResource>:
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.4" />
</ItemGroup>

<ItemGroup>
<EmbeddedResource Include="Resource.txt" />
</ItemGroup>

</Project>

Use patrones globales para especificar uno o varios archivos para incrustar en el ensamblado.
La aplicación de ejemplo crea ManifestEmbeddedFileProvider y pasa el ensamblado que se está ejecutando
actualmente a su constructor.
Startup.cs:

var manifestEmbeddedProvider =
new ManifestEmbeddedFileProvider(Assembly.GetEntryAssembly());

Las sobrecargas adicionales le permiten:


Especificar una ruta de acceso de archivo relativa.
Definir el ámbito de archivos a la fecha de la última modificación.
Asignar nombre al recurso incrustado que contiene el manifiesto del archivo incrustado.

SOBRECARGA DESCRIPCIÓN

ManifestEmbeddedFileProvider(Assembly, String) Acepta parámetro de ruta de acceso relativa root opcional.


Especifique root para definir el ámbito de las llamadas en
GetDirectoryContents en aquellos recursos que se encuentran
bajo las rutas de acceso proporcionadas.

ManifestEmbeddedFileProvider(Assembly, String, Acepta un parámetro de ruta de acceso relativa root


DateTimeOffset) opcional y un parámetro de fecha lastModified
(DateTimeOffset). La fecha lastModified define el ámbito
de la última fecha de modificación para las instancias de
IFileInfo que IFileProvider devuelve.

ManifestEmbeddedFileProvider(Assembly, String, String, Acepta una ruta de acceso relativa root opcional, una fecha
DateTimeOffset) lastModified y parámetros manifestName .
manifestName representa el nombre del recurso incrustado
que contiene el manifiesto.

EmbeddedFileProvider
EmbeddedFileProvider se utiliza para acceder a archivos incrustados dentro de ensamblados. Especifique los
archivos que desea incrustar con la propiedad <EmbeddedResource> propiedad en el archivo de proyecto:
<ItemGroup>
<EmbeddedResource Include="Resource.txt" />
</ItemGroup>

Use patrones globales para especificar uno o varios archivos para incrustar en el ensamblado.
La aplicación de ejemplo crea EmbeddedFileProvider y pasa el ensamblado que se está ejecutando actualmente a
su constructor.
Startup.cs:

var embeddedProvider = new EmbeddedFileProvider(Assembly.GetEntryAssembly());

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 . . En la aplicación de ejemplo, baseNamespace es
FileProviderSample. .

El constructor EmbeddedFileProvider(Assembly, String) acepta un parámetro baseNamespace opcional. Especifique


el espacio de nombres base para definir el ámbito de las llamadas en GetDirectoryContents en aquellos recursos
que se encuentran bajo 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.

En la aplicación de ejemplo, PhysicalFileProvider y ManifestEmbeddedFileProvider proporcionan archivos a un


CompositeFileProvider registrado en el contenedor de servicios de la aplicación:

var physicalProvider = _env.ContentRootFileProvider;


var manifestEmbeddedProvider =
new ManifestEmbeddedFileProvider(Assembly.GetEntryAssembly());
var compositeProvider =
new CompositeFileProvider(physicalProvider, manifestEmbeddedProvider);

services.AddSingleton<IFileProvider>(compositeProvider);

En la aplicación de ejemplo, PhysicalFileProvider y EmbeddedFileProvider proporcionan archivos a un


CompositeFileProvider registrado en el contenedor de servicios de la aplicación:

var physicalProvider = _hostingEnvironment.ContentRootFileProvider;


var embeddedProvider = new EmbeddedFileProvider(Assembly.GetEntryAssembly());
var compositeProvider = new CompositeFileProvider(physicalProvider, embeddedProvider);

services.AddSingleton<IFileProvider>(compositeProvider);

Observación de cambios
El método IFileProvider.Watch proporciona un escenario para ver uno o más archivos o directorios para detectar
cambios. Watch acepta una cadena de ruta de acceso, que puede usar patrones globales para especificar varios
archivos. Watch devuelve IChangeToken. El token de cambio expone:
HasChanged: una propiedad que se puede inspeccionar para determinar si se ha producido un cambio.
RegisterChangeCallback: se llama cuando se detectan cambios en la cadena de ruta de acceso especificada.
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 (como se muestra a continuación) o
volver a crear instancias de IChangeToken en respuesta a los cambios.

En la aplicación de ejemplo, la aplicación de consola WatchConsole se configura 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");
}

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

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_POLLING_FILE_WATCHER
en 1 o true para sondear el sistema de archivos en busca de cambios cada cuatro segundos (no configurable).
Patrones globales
Las rutas de acceso del sistema de archivos utilizan patrones de caracteres comodín denominados patrones
globales. Especifique grupos de archivos con estos patrones. 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 globales
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/*/appsettings.json
Coincide con todos los archivos appsettings.json que estén en directorios exactamente un nivel por debajo de la
carpeta directorio.
directory/**/*.txt
Coincide con todos los archivos que tengan la extensión .txt y se encuentren en cualquier lugar de la carpeta
directorio.
Patrón de repositorio con ASP.NET Core
06/08/2018 • 6 minutes to read • Edit Online

Por Steve Smith, Scott Addie y Luke Latham


El patrón de repositorio es un patrón de diseño que aísla el acceso a datos subyacente a las abstracciones de
interfaz. La conexión a la base de datos y la manipulación de objetos de almacenamiento de datos se realizan a
través de los métodos proporcionados por la implementación de la interfaz. En consecuencia, no hay ninguna
necesidad de llamar a código para tratar problemas de la base de datos, como conexiones, comandos y lectores.
La implementación del patrón de repositorio con ASP.NET Core tiene las siguientes ventajas:
La organización de la aplicación es menos compleja sin ninguna interdependencia directa entre el negocio y
los niveles de acceso a datos.
Es más fácil reutilizar el código de acceso de base de datos porque el código se administra centralmente
mediante uno o varios repositorios.
El dominio de negocio puede ser una unidad probada de forma independiente desde la capa de la base de
datos.
Vea o descargue el código de ejemplo (cómo descargarlo)
La aplicación de ejemplo usa el patrón de repositorio para inicializar y mostrar una lista de nombres de
personajes de una película. La aplicación usa Entity Framework Core y una clase ApplicationDbContext para su
persistencia de datos, pero la infraestructura de la base de datos no es evidente cuando se tiene acceso a los
datos. El acceso a los datos y los objeto de base de datos se extraen detrás de un repositorio.

Interfaz del repositorio


Una interfaz de repositorio define las propiedades y los métodos de la implementación. En la aplicación de
ejemplo, la interfaz de repositorio para los datos de personajes de una película es ICharacterRepository .
ICharacterRepository define los métodos ListAll y Add requeridos para trabajar con instancias Character en
la aplicación:

public interface ICharacterRepository


{
IEnumerable<Character> ListAll();
void Add(Character character);
}

public interface ICharacterRepository


{
IEnumerable<Character> ListAll();
void Add(Character character);
}

Character se define como:


public class Character
{
public Character(string name)
{
Name = name;
}

[Key]
public Guid Id { get; private set; } = Guid.NewGuid();
public string Name { get; private set; } = String.Empty;
}

public class Character


{
public Character(string name)
{
Name = name;
}

private Character()
{
}

[Key]
public Guid Id { get; private set; } = Guid.NewGuid();
public string Name { get; private set; } = String.Empty;
}

Tipo de repositorio concreto


Esta interfaz se implementa mediante un tipo concreto. En la aplicación de ejemplo, CharacterRepository
administra el contexto de base de datos e implementa los métodos ListAll y Add :

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

Registro del servicio de repositorio


El repositorio y el contexto de la base de datos se registran con el contenedor de servicios en
Startup.ConfigureServices . En la aplicación de ejemplo, ApplicationDbContext se configura con la llamada al
método de extensión AddDbContext. ICharacterRepository se registra como un servicio con ámbito:

public void ConfigureServices(IServiceCollection services)


{
// Add database services.
services.AddDbContext<ApplicationDbContext>(options =>
options.UseInMemoryDatabase("InMemoryDb")
);

// Add framework services.


services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});

services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

// Register application services.


services.AddScoped<ICharacterRepository, CharacterRepository>();
}

public void ConfigureServices(IServiceCollection services)


{
// Add database services.
services.AddDbContext<ApplicationDbContext>(options =>
options.UseInMemoryDatabase()
);

// Add framework services.


services.AddMvc();

// Register application services.


services.AddScoped<ICharacterRepository, CharacterRepository>();
}
Inyección de una instancia del repositorio
En una clase donde se requiere acceso de la base de datos, una instancia del repositorio se solicita mediante el
constructor y se asigna a un campo privado para su uso en los métodos de clase. En la aplicación de ejemplo, se
utiliza ICharacterRepository para:
Rellene la base de datos si no existe ningún carácter.
Obtenga una lista de los personajes para su presentación.
Tenga en cuenta que el código de llamada solo interactúa con la implementación de la interfaz,
CharacterRepository . El código de llamada no usa ApplicationDbContext directamente:

public class IndexModel : PageModel


{
private readonly ICharacterRepository _characterRepository;

public IndexModel(ICharacterRepository characterRepository)


{
_characterRepository = characterRepository;
}

public IEnumerable<Character> Characters { get; set; }

public void OnGet()


{
PopulateCharactersIfNoneExist();

Characters = _characterRepository.ListAll();
}

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"));
}
}
}
public class HomeController : Controller
{
private readonly ICharacterRepository _characterRepository;

public HomeController(ICharacterRepository characterRepository)


{
_characterRepository = characterRepository;
}

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

Interfaz de repositorio genérica


En este tema y en su aplicación de ejemplo se muestra la implementación más sencilla del patrón de repositorio,
donde se crea un repositorio para cada objeto de negocio. Si la aplicación crece más allá de unos pocos objetos,
una interfaz de repositorio genérica puede reducir la cantidad de código necesario para implementar el patrón
de repositorio. Para más información, vea DevIQ: Repository Pattern: Generic Repository Interface (DevIQ:
Patrón de repositorio: interfaz de repositorio genérica).

Recursos adicionales
DevIQ: Patrón de repositorio
Inserción de dependencias
Inserción de dependencias en vistas
Inserción de dependencias en controladores
Inserción de dependencias en controladores de requisitos
Inversión de los contenedores de control y el patrón de inserción de dependencias
Globalización y localización en ASP.NET Core
06/08/2018 • 33 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
Vea o descargue el código de ejemplo (cómo descargarlo)

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.
RootNamespaceAttribute
El atributo RootNamespace proporciona el espacio de nombres raíz de un ensamblado cuando el espacio de
nombres raíz del ensamblado es diferente del nombre de ensamblado.
Si el espacio de nombres raíz de un ensamblado es diferente del nombre de ensamblado:
La localización no funciona de forma predeterminada.
Se produce un error en la localización debido a la manera en que se buscan los recursos dentro del
ensamblado. RootNamespace es un valor en tiempo de compilación que no está disponible para el proceso
en ejecución.
Si RootNamespace es diferente de AssemblyName , incluya lo siguiente en AssemblyInfo.cs (con los valores de
parámetro reemplazados por los valores reales):

using System.Reflection;
using Microsoft.Extensions.Localization;

[assembly: ResourceLocation("Resource Folder Name")]


[assembly: RootNamespace("App Root Namespace")]

El código anterior permite la resolución correcta de los archivos resx.

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
27/08/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 puede 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.
Vea o descargue el código de ejemplo (cómo descargarlo)

Requisitos previos
Los proyectos para .NET Framework requieren instalar el paquete NuGet Microsoft.Extensions.Http. Los
proyectos para .NET Core y que hagan referencia al metapaquete Microsoft.AspNetCore.App ya incluyen el
paquete Microsoft.Extensions.Http .

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 Startup.ConfigureServices .

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 Startup.ConfigureServices .

services.AddHttpClient("github", c =>
{
c.BaseAddress = new Uri("https://api.github.com/");
// Github API versioning
c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
// Github requires a user-agent
c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});

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/");
// GitHub API versioning
client.DefaultRequestHeaders.Add("Accept",
"application/vnd.github.v3+json");
// GitHub requires a user-agent
client.DefaultRequestHeaders.Add("User-Agent",
"HttpClientFactory-Sample");

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


Startup.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
{
// _httpClient isn't exposed publicly
private readonly HttpClient _httpClient;

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 =>
{
// Assume this is an "external" service which requires an API KEY
c.BaseAddress = new Uri("https://localhost:5000/");
})
.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 el paquete NuGet
Microsoft.Extensions.Http.Polly. Este paquete no está incluido en el metapaquete Microsoft.AspNetCore.App. Para
usar las extensiones, se debe incluir un <PackageReference /> explícito en el proyecto.

<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.4" />
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="2.1.1" />
</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 se producen cuando las llamadas HTTP externas son transitorias. 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 Startup.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 registran dos directivas cuando se agrega PolicyRegistry a ServiceCollection . Para 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 agrupa 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.

Se recomienda agrupar controladores porque cada uno de ellos normalmente administra sus propias conexiones
HTTP subyacentes. Crear más controladores de los necesarios 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 la interfaz
IHttpClientBuilder que se devuelve cuando se crea el cliente:

services.AddHttpClient("extendedhandlerlifetime")
.SetHandlerLifetime(TimeSpan.FromMinutes(5));

No hace falta eliminar el cliente, ya que se cancelan las solicitudes salientes y la instancia de HttpClient
determinada no se puede usar después de llamar a Dispose. IHttpClientFactory realiza un seguimiento y elimina
los recursos que usan las instancias de HttpClient . Normalmente, las instancias de HttpClient pueden tratarse
como objetos de .NET que no requieren eliminación.
Mantener una sola instancia de HttpClient activa durante un período prolongado es un patrón común que se
utiliza antes de la concepción de IHttpClientFactory . Este patrón se convierte en innecesario tras la migración a
IHttpClientFactory .

Registro
Los clientes que se han creado a través de IHttpClientFactory registran mensajes de registro de todas las
solicitudes. Habilite 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.
Al habilitar 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. El método de extensión
ConfigurePrimaryHttpMessageHandler puede utilizarse 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 )
Acceso a HttpContext en ASP.NET Core
02/08/2018 • 2 minutes to read • Edit Online

Las aplicaciones ASP.NET Core acceden a HttpContext a través de la interfaz IHttpContextAccessor y de su


implementación predeterminada HttpContextAccessor. Solo es necesario utilizar IHttpContextAccessor si necesita
acceder al valor HttpContext dentro de un servicio.

Uso de HttpContext desde Razor Pages


PageModel de Razor Pages expone la propiedad HttpContext:

public class AboutModel : PageModel


{
public string Message { get; set; }

public void OnGet()


{
Message = HttpContext.Request.PathBase;
}
}

Uso de HttpContext desde una vista de Razor


Las vistas de Razor exponen el valor HttpContext directamente mediante una propiedad RazorPage.Context de la
vista. En el ejemplo siguiente se recupera el nombre de usuario actual de una aplicación de Intranet mediante la
Autenticación de Windows:

@{
var username = Context.User.Identity.Name;
}

Uso de HttpContext desde un controlador


Los controladores exponen la propiedad ControllerBase.HttpContext:

public class HomeController : Controller


{
public IActionResult About()
{
var pathBase = HttpContext.Request.PathBase;
// Do something with the PathBase.

return View();
}
}

Uso de HttpContext desde el software intermedio


Cuando se trabaja con componentes de software intermedio personalizado, HttpContext se pasa al método
Invoke o InvokeAsync y es accesible cuando el software intermedio está configurado:
public class MyCustomMiddleware
{
public Task InvokeAsync(HttpContext context)
{
// Middleware initialization optionally using HttpContext
}
}

Uso de HttpContext desde componentes personalizados


Para los componentes de otro marco y componentes personalizados que requieren acceso a HttpContext , el
enfoque recomendado es registrar una dependencia mediante el contenedor integrado de inserción de
dependencias. El contenedor de inserción de dependencias proporciona IHttpContextAccessor a cualquier clase
que la declare como una dependencia en sus constructores.

public void ConfigureServices(IServiceCollection services)


{
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddHttpContextAccessor();
services.AddTransient<IUserRepository, UserRepository>();
}

public void ConfigureServices(IServiceCollection services)


{
services.AddMvc();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddTransient<IUserRepository, UserRepository>();
}

En el ejemplo anterior:
UserRepository declara su dependencia de IHttpContextAccessor .
La dependencia se proporciona cuando la inserción de dependencias resuelve la cadena de dependencias y crea
una instancia de UserRepository .

public class UserRepository : IUserRepository


{
private readonly IHttpContextAccessor _httpContextAccessor;

public UserRepository(IHttpContextAccessor httpContextAccessor)


{
_httpContextAccessor = httpContextAccessor;
}

public void LogCurrentUser()


{
var username = _httpContextAccessor.HttpContext.User.Identity.Name;
service.LogAccessRequest(username);
}
}
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
27/08/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. En el caso de las aplicaciones que no usan el metapaquete
Microsoft.AspNetCore.App (ASP.NET Core 2.1 o posterior), 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. En el caso de las
aplicaciones que no usan el metapaquete Microsoft.AspNetCore.App, 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.

Recursos adicionales
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
31/08/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.

NOTE
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 Microsoft.AspNetCore.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 Core
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 Core 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 Core en un servidor basado en OWIN


Los servidores basados en OWIN pueden hospedar aplicaciones ASP.NET Core. 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) DESCRIPCIÓN

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) DESCRIPCIÓN

owin.RequestBody Stream

Datos de solicitud (OWIN v1.1.0)


KEY VALOR (TIPO) DESCRIPCIÓN

owin.RequestId String Optional

Datos de respuesta (OWIN v1.0.0)


KEY VALOR (TIPO) DESCRIPCIÓN

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) DESCRIPCIÓN

owin.CallCancelled CancellationToken

owin.Version String

Claves comunes
KEY VALOR (TIPO) DESCRIPCIÓN

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) DESCRIPCIÓN

sendfile.SendAsync Vea Delegate signature (Signatura de Por solicitud


delegado)

Opaque v0.3.0
KEY VALOR (TIPO) DESCRIPCIÓN

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) DESCRIPCIÓN

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
11/07/2018 • 8 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.
SignalR de ASP.NET Core es una biblioteca que simplifica la adición de la funcionalidad web en tiempo real a las
aplicaciones. Usa WebSockets siempre que sea posible.

Cómo usar WebSockets


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
17/09/2018 • 5 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 , ningún recurso de los paquetes NuGet de ASP.NET Core a los que se
hace referencia se implementa con la aplicación: el marco compartido de ASP.NET Core 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 archivo de proyecto siguiente hace referencia al metapaquete Microsoft.AspNetCore.App de
ASP.NET Core y representa una típica plantilla de ASP.NET Core 2.1:
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.4" />
</ItemGroup>

</Project>

El número de versión en la referencia de Microsoft.AspNetCore.App no garantiza que se vaya a usar


la versión del marco compartido. Por ejemplo, suponga que se ha especificado la versión 2.1.1 ,
pero se ha instalado la 2.1.3 . En ese caso, la aplicación usa 2.1.3 . Aunque no se recomienda,
puede deshabilitar el comportamiento de puesta al día (revisión o secundaria). Para obtener más
información sobre el comportamiento de puesta al día de la versión del paquete, vea dotnet host
roll forward (puesta al día del host de dotnet).

Actualización de ASP.NET Core


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.
Para actualizar ASP.NET Core:
En los equipos de desarrollo y los servidores de compilación: descargue e instale el SDK de
.NET Core.
En los servidores de implementación: descargue e instale el .NET Core Runtime.
Las aplicaciones se pondrán al día con la última versión instalada al reiniciar la aplicación. No es
necesario actualizar el número de versión de Microsoft.AspNetCore.App en el archivo de proyecto.
Para obtener más información, vea Puesta al día de las aplicaciones dependientes de la plataforma.
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
23/07/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.1.4" />
</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
Elegir entre ASP.NET y ASP.NET Core
25/06/2018 • 2 minutes to read • Edit Online

Independientemente de la aplicación web que vaya a crear, ASP.NET tiene una solución para usted: desde
aplicaciones web empresariales para Windows Server hasta pequeños microservicios para contenedores de Linux,
y mucho más.

ASP.NET Core
ASP.NET Core es un marco multiplataforma de código abierto que tiene como finalidad compilar modernas
aplicaciones web basadas en la nube en Windows, macOS o Linux.

ASP.NET
ASP.NET es un marco consolidado que proporciona todos los servicios necesarios para compilar aplicaciones web
empresariales basadas en servidor en Windows.

Selección del marco


Revise la siguiente tabla para averiguar qué marco es el más adecuado según sus necesidades.

ASP.NET CORE ASP.NET

Compilación para Windows, macOS o Linux Compilación para Windows

Las páginas de Razor son el método recomendado para crear Use formularios Web Forms, SignalR, MVC, Web API,
una interfaz de usuario web desde la aparición de ASP.NET WebHooks o Web Pages
Core 2.x. Vea también MVC, Web API y SignalR.

Varias versiones por equipo Una versión por equipo

Desarrollo con Visual Studio, Visual Studio para Mac o Visual Desarrollo con Visual Studio con C#, VB o F#
Studio Code con C# o F#

Mayor rendimiento que ASP.NET Buen rendimiento

Elegir .NET Framework o .NET Core Usar el tiempo de ejecución de .NET Framework

Escenarios de ASP.NET Core


Las páginas de Razor son el método recomendado para crear una interfaz de usuario web desde la aparición de
ASP.NET Core 2.x.
Sitios web
API
En tiempo real

Escenarios de ASP.NET
Sitios web
API
En tiempo real

Recursos
Introducción a ASP.NET
Introducción a ASP.NET Core
Introducción a las páginas de Razor en ASP.NET
Core
23/08/2018 • 36 minutes to read • Edit Online

Por Rick Anderson y Ryan Nowak


Las páginas de Razor son una nueva característica de ASP.NET Core MVC que facilita la codificación de
escenarios centrados en páginas y hace que sea más productiva.
Si busca un tutorial que use el enfoque Model-View -Controller, consulte Introducción a ASP.NET Core
MVC.
En este documento se proporciona una introducción a las páginas de Razor. No es un tutorial paso a
paso. Si encuentra que alguna sección es demasiado avanzada, consulte Introducción a las páginas de
Razor. Para obtener información general de ASP.NET Core, vea Introducción a ASP.NET Core.

Requisitos previos
Instale uno de los siguientes:
Herramientas de CLI: Windows, Linux o macOS: .NET Core SDK 2.0 o posterior
Herramientas de IDE/editor
Windows: Visual Studio para Windows
Carga de trabajo de ASP.NET y desarrollo web
Carga de trabajo Desarrollo multiplataforma de .NET Core
Linux: Visual Studio Code
macOS: Visual Studio para Mac

Crear un proyecto de páginas de Razor


Visual Studio
Visual Studio para Mac
Visual Studio Code
CLI de .NET Core
Vea Introducción a las páginas de Razor para obtener instrucciones detalladas sobre cómo crear un
proyecto de páginas de Razor con Visual Studio.

Páginas de Razor
Páginas de Razor está habilitada en Startup.cs:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// Includes support for Razor Pages and controllers.
services.AddMvc();
}

public void Configure(IApplicationBuilder app)


{
app.UseMvc();
}
}

Considere la posibilidad de una página básica:

@page

<h1>Hello, world!</h1>
<h2>The time on the server is @DateTime.Now</h2>

El código anterior es muy parecido a un archivo de vista de Razor. La directiva @page lo hace diferente.
@page transforma el archivo en una acción de MVC, lo que significa que administra las solicitudes
directamente, sin tener que pasar a través de un controlador. @page debe ser la primera directiva de
Razor de una página. @page afecta al comportamiento de otras construcciones de Razor.
Una página similar, con una clase PageModel , se muestra en los dos archivos siguientes. El archivo
Pages/Index2.cshtml:

@page
@using RazorPagesIntro.Pages
@model IndexModel2

<h2>Separate page model</h2>


<p>
@Model.Message
</p>

Modelo de página Pages/Index2.cshtml.cs:

using Microsoft.AspNetCore.Mvc.RazorPages;
using System;

namespace RazorPagesIntro.Pages
{
public class IndexModel2 : PageModel
{
public string Message { get; private set; } = "PageModel in C#";

public void OnGet()


{
Message += $" Server time is { DateTime.Now }";
}
}
}

Por convención, el archivo de clase PageModel tiene el mismo nombre que el archivo de páginas de
Razor con .cs anexado. Por ejemplo, la página de Razor anterior es Pages/Index2.cshtml. El archivo que
contiene la clase PageModel se denomina Pages/Index2.cshtml.cs.
Las asociaciones de rutas de dirección URL a páginas se determinan según la ubicación de la página en
el sistema de archivos. En la tabla siguiente, se muestra una ruta de acceso de página de Razor y la
dirección URL correspondiente:

RUTA DE ACCESO Y NOMBRE DE ARCHIVO URL CORRESPONDIENTE

/Pages/Index.cshtml / o /Index

/Pages/Contact.cshtml /Contact

/Pages/Store/Contact.cshtml /Store/Contact

/Pages/Store/Index.cshtml /Store o /Store/Index

Notas:
El runtime busca archivos de páginas de Razor en la carpeta Páginas de forma predeterminada.
Index es la página predeterminada cuando una URL no incluye una página.

Escribir un formulario básico


Las páginas de Razor están diseñadas para facilitar la implementación de patrones comunes que se usan
con exploradores web al compilar una aplicación. Los enlaces de modelos, las aplicaciones auxiliares de
etiquetas y las aplicaciones auxiliares de HTML simplemente funcionan con las propiedades definidas en
una clase de página de Razor. Considere la posibilidad de una página que implementa un formulario
básico del estilo "Póngase en contacto con nosotros" para el modelo Contact :
Para los ejemplos de este documento, DbContext se inicializa en el archivo Startup.cs.

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using RazorPagesContacts.Data;

namespace RazorPagesContacts
{
public class Startup
{
public IHostingEnvironment HostingEnvironment { get; }

public void ConfigureServices(IServiceCollection services)


{
services.AddDbContext<AppDbContext>(options =>
options.UseInMemoryDatabase("name"));
services.AddMvc();
}

public void Configure(IApplicationBuilder app)


{
app.UseMvc();
}
}
}

El modelo de datos:
using System.ComponentModel.DataAnnotations;

namespace RazorPagesContacts.Data
{
public class Customer
{
public int Id { get; set; }

[Required, StringLength(100)]
public string Name { get; set; }
}
}

El contexto de la base de datos:

using Microsoft.EntityFrameworkCore;

namespace RazorPagesContacts.Data
{
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions options)
: base(options)
{
}

public DbSet<Customer> Customers { get; set; }


}
}

El archivo de vista Pages/Create.cshtml:

@page
@model RazorPagesContacts.Pages.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<html>
<body>
<p>
Enter your name.
</p>
<div asp-validation-summary="All"></div>
<form method="POST">
<div>Name: <input asp-for="Customer.Name" /></div>
<input type="submit" />
</form>
</body>
</html>

Modelo de página Pages/Create.cshtml.cs:


using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesContacts.Data;

namespace RazorPagesContacts.Pages
{
public class CreateModel : PageModel
{
private readonly AppDbContext _db;

public CreateModel(AppDbContext db)


{
_db = db;
}

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

public async Task<IActionResult> OnPostAsync()


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

_db.Customers.Add(Customer);
await _db.SaveChangesAsync();
return RedirectToPage("/Index");
}
}
}

Por convención, la clase PageModel se denomina <PageName>Model y se encuentra en el mismo espacio


de nombres que la página.
La clase PageModel permite la separación de la lógica de una página de su presentación. Define los
controladores de página para solicitudes que se envían a la página y los datos que usan para representar
la página. Esta separación le permite administrar dependencias de páginas mediante la inyección de
dependencias y para realizar pruebas unitarias de las páginas.
La página tiene un método de controlador OnPostAsync , que se ejecuta en solicitudes POST (cuando un
usuario envía el formulario). Puede agregar métodos de controlador para cualquier verbo HTTP. Los
controladores más comunes son:
OnGet para inicializar el estado necesario para la página. Ejemplo OnGet.
OnPost para controlar los envíos del formulario.

El sufijo de nombre Async es opcional, pero se usa a menudo por convención para funciones
asincrónicas. El código OnPostAsync en el ejemplo anterior es similar a lo que escribiría normalmente en
un controlador. El código anterior es típico de las páginas de Razor. La mayoría de primitivos MVC como
el enlace de modelos, la validación y los resultados de acciones se comparten.
El método OnPostAsync anterior:
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}

_db.Customers.Add(Customer);
await _db.SaveChangesAsync();
return RedirectToPage("/Index");
}

El flujo básico de OnPostAsync :


Compruebe los errores de validación.
Si no hay ningún error, guarde los datos y redirija.
Si hay errores, muestre la página de nuevo con mensajes de validación. La validación del lado cliente
es idéntica a las aplicaciones de ASP.NET Core MVC tradicionales. En muchos casos, los errores de
validación se detectan en el cliente y nunca se envían al servidor.
Cuando los datos se escriben correctamente, el método del controlador OnPostAsync llama al método
auxiliar RedirectToPage para devolver una instancia de RedirectToPageResult . RedirectToPage es un
resultado de acción nueva, similar a RedirectToAction o RedirectToRoute , pero personalizada para las
páginas. En el ejemplo anterior, redirige a la página de índice raíz ( /Index ). RedirectToPage se detalla en
la sección Generación de direcciones URL para las páginas.
Cuando el formulario enviado tiene errores de validación (que se pasan al servidor), el método del
controlador OnPostAsync llama al método auxiliar Page . Page devuelve una instancia de PageResult .
Devolver Page es similar a cómo las acciones en los controladores devuelven View . PageResult es el
tipo de valor devuelto predeterminado para un método de controlador. Un método de controlador que
devuelve void representa la página.
La propiedad Customer usa el atributo [BindProperty] para participar en el enlace de modelos.

public class CreateModel : PageModel


{
private readonly AppDbContext _db;

public CreateModel(AppDbContext db)


{
_db = db;
}

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

public async Task<IActionResult> OnPostAsync()


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

_db.Customers.Add(Customer);
await _db.SaveChangesAsync();
return RedirectToPage("/Index");
}
}
De forma predeterminada, las páginas de Razor enlazan propiedades solo con verbos que no sean GET.
Enlazar a propiedades puede reducir la cantidad de código que se debe escribir. Enlazar reduce el código
al usar la misma propiedad para representar los campos de formulario (
<input asp-for="Customer.Name" /> ) y aceptar la entrada.

NOTE
Por motivos de seguridad, debe participar en el enlace de datos de solicitud GET con las propiedades del modelo
de página. Compruebe las entradas de los usuarios antes de asignarlas a las propiedades. Si participa en este
comportamiento, le puede ser útil al trabajar con escenarios que dependan de cadenas de consultas o valores de
rutas.
Para enlazar una propiedad en solicitudes GET, establezca la propiedad SupportsGet del atributo
[BindProperty] como true : [BindProperty(SupportsGet = true)]

La página principal (Index.cshtml):

@page
@model RazorPagesContacts.Pages.IndexModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<h1>Contacts</h1>
<form method="post">
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
</tr>
</thead>
<tbody>
@foreach (var contact in Model.Customers)
{
<tr>
<td>@contact.Id</td>
<td>@contact.Name</td>
<td>
<a asp-page="./Edit" asp-route-id="@contact.Id">edit</a>
<button type="submit" asp-page-handler="delete"
asp-route-id="@contact.Id">delete</button>
</td>
</tr>
}
</tbody>
</table>

<a asp-page="./Create">Create</a>
</form>

La clase PageModel asociada (Index.cshtml.cs):


using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesContacts.Data;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;

namespace RazorPagesContacts.Pages
{
public class IndexModel : PageModel
{
private readonly AppDbContext _db;

public IndexModel(AppDbContext db)


{
_db = db;
}

public IList<Customer> Customers { get; private set; }

public async Task OnGetAsync()


{
Customers = await _db.Customers.AsNoTracking().ToListAsync();
}

public async Task<IActionResult> OnPostDeleteAsync(int id)


{
var contact = await _db.Customers.FindAsync(id);

if (contact != null)
{
_db.Customers.Remove(contact);
await _db.SaveChangesAsync();
}

return RedirectToPage();
}
}
}

El archivo Index.cshtml contiene el siguiente marcado para crear un vínculo de edición para cada
contacto:

<a asp-page="./Edit" asp-route-id="@contact.Id">edit</a>

La aplicación auxiliar de etiquetas delimitadoras ha usado el atributo asp-route-{value} para generar un


vínculo a la página de edición. El vínculo contiene datos de ruta con el identificador del contacto. Por
ejemplo: http://localhost:5000/Edit/1 .
El archivo Pages/Edit.cshtml:
@page "{id:int}"
@model RazorPagesContacts.Pages.EditModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

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

<h1>Edit Customer - @Model.Customer.Id</h1>


<form method="post">
<div asp-validation-summary="All"></div>
<input asp-for="Customer.Id" type="hidden" />
<div>
<label asp-for="Customer.Name"></label>
<div>
<input asp-for="Customer.Name" />
<span asp-validation-for="Customer.Name" ></span>
</div>
</div>

<div>
<button type="submit">Save</button>
</div>
</form>

La primera línea contiene la directiva @page "{id:int}" . La restricción de enrutamiento "{id:int}"


indica a la página que acepte las solicitudes a la página que contienen datos de ruta int . Si una solicitud
a la página no contiene datos de ruta que se puedan convertir en int , el tiempo de ejecución devuelve
un error HTTP 404 (no encontrado). Para que el identificador sea opcional, anexe ? a la restricción de
ruta:

@page "{id:int?}"

El archivo Pages/Edit.cshtml.cs:
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using RazorPagesContacts.Data;

namespace RazorPagesContacts.Pages
{
public class EditModel : PageModel
{
private readonly AppDbContext _db;

public EditModel(AppDbContext db)


{
_db = db;
}

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

public async Task<IActionResult> OnGetAsync(int id)


{
Customer = await _db.Customers.FindAsync(id);

if (Customer == null)
{
return RedirectToPage("/Index");
}

return Page();
}

public async Task<IActionResult> OnPostAsync()


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

_db.Attach(Customer).State = EntityState.Modified;

try
{
await _db.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
throw new Exception($"Customer {Customer.Id} not found!");
}

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

El archivo index.cshtml también contiene una marca para crear un botón de eliminar para cada contacto
de cliente:

<button type="submit" asp-page-handler="delete"


asp-route-id="@contact.Id">delete</button>

Al representar dicho botón de eliminar en HTML, formaction incluye parámetros para:


Id. de contacto de cliente especificado mediante el atributo asp-route-id .
handler especificado mediante el atributo asp-page-handler .

Aquí tiene un ejemplo de un botón de eliminar representado con un id. de contacto de cliente de 1 :

<button type="submit" formaction="/?id=1&amp;handler=delete">delete</button>

Al seleccionar el botón, se envía una solicitud de formulario POST al servidor. De forma predeterminada,
el nombre del método de control se selecciona de acuerdo con el valor del parámetro handler y según
el esquema OnPost[handler]Async .
Como en este ejemplo handler es delete , el método de control OnPostDeleteAsync se usa para
procesar la solicitud POST . Si asp-page-handler se establece en otro valor, como remove , se seleccionará
un método de control de páginas con el nombre OnPostRemoveAsync .

public async Task<IActionResult> OnPostDeleteAsync(int id)


{
var contact = await _db.Customers.FindAsync(id);

if (contact != null)
{
_db.Customers.Remove(contact);
await _db.SaveChangesAsync();
}

return RedirectToPage();
}

El método OnPostDeleteAsync realiza las acciones siguientes:


Acepta el elemento id de la cadena de consulta.
Realiza una consulta a la base de datos del contacto de cliente con FindAsync .
Si encuentra dicho contacto de cliente, se quita de la lista correspondiente. Luego, se actualiza la base
de datos.
Llama a RedirectToPage para redirigir la página Index raíz ( /Index ).

Es necesario marcar las propiedades de página


Las propiedades de un valor PageModel se pueden decorar con el atributo Required:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.ComponentModel.DataAnnotations;

namespace RazorPagesMovie.Pages.Movies
{
public class CreateModel : PageModel
{
public IActionResult OnGet()
{
return Page();
}

[BindProperty]
[Required(ErrorMessage = "Color is required")]
public string Color { get; set; }

public IActionResult OnPostAsync()


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

// Process color.

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

Para más información, vea Validación de modelos.

Administración de solicitudes HEAD con el controlador OnGet


Normalmente, se crea un controlador HEAD al que se llama para las solicitudes HEAD:

public void OnHead()


{
HttpContext.Response.Headers.Add("HandledBy", "Handled by OnHead!");
}

Si no se define ningún controlador HEAD ( OnHead ), las páginas de Razor vuelven a llamar al controlador
de páginas GET ( OnGet ) en ASP.NET Core 2.1 o posterior. Tiene la opción de usar este comportamiento
con el método SetCompatibilityVersion en Startup.Configure para ASP.NET Core 2.1 a 2.x:

services.AddMvc()
.SetCompatibilityVersion(Microsoft.AspNetCore.Mvc.CompatibilityVersion.Version_2_1);

SetCompatibilityVersion define con eficacia la opción de las páginas de Razor


AllowMappingHeadRequestsToGetHandler como true .

En lugar de participar en todos los comportamientos de 2.1 con SetCompatibilityVersion , puede


participar explícitamente en comportamientos específicos. El código que se indica a continuación
participa en las solicitudes HEAD de asignación del controlador GET.
services.AddMvc()
.AddRazorPagesOptions(options =>
{
options.AllowMappingHeadRequestsToGetHandler = true;
});

XSRF/CSRF y páginas de Razor


No tiene que escribir ningún código para la validación antifalsificación. La validación y generación de
tokens antifalsificación se incluyen automáticamente en las páginas de Razor.

Usar diseños, parciales, plantillas y aplicaciones auxiliares de


etiquetas con las páginas de Razor
Las páginas funcionan con todas las características del motor de vista de Razor. Los diseños, parciales,
plantillas, aplicaciones auxiliares de etiquetas, _ViewStart.cshtml, _ViewImports.cshtml funcionan de la
misma manera que lo hacen con las vistas de Razor convencionales.
Para simplificar esta página, aprovecharemos algunas de esas características.
Agregue una página de diseño a Pages/Shared/_Layout.cshtml:
Agregue una página de diseño a Pages/_Layout.cshtml:

<!DOCTYPE html>
<html>
<head>
<title>Razor Pages Sample</title>
</head>
<body>
<a asp-page="/Index">Home</a>
@RenderBody()
<a asp-page="/Customers/Create">Create</a> <br />
</body>
</html>

El diseño:
Controla el diseño de cada página (a no ser que la página no tenga diseño).
Importa las estructuras HTML como JavaScript y hojas de estilos.
Vea Layout page (Página de diseño) para obtener más información.
La propiedad Layout se establece en Pages/_ViewStart.cshtml:

@{
Layout = "_Layout";
}

El diseño está en la carpeta Pages/Shared. Las páginas buscan otras vistas (diseños, plantillas, parciales)
de forma jerárquica, a partir de la misma carpeta que la página actual. Un diseño en la carpeta
Pages/Shared se puede usar desde cualquier página de Razor en la carpeta Pages.
El archivo de diseño debería ir en la carpeta Pages/Shared.
El diseño está en la carpeta Pages. Las páginas buscan otras vistas (diseños, plantillas, parciales) de
forma jerárquica, a partir de la misma carpeta que la página actual. Un diseño en la carpeta Pages se
puede usar desde cualquier página de Razor en la carpeta Pages.
Le recomendamos que no coloque el archivo de diseño en la carpeta Views/Shared. Views/Shared es un
patrón de vistas de MVC. Las páginas de Razor están diseñadas para basarse en la jerarquía de carpetas,
no en las convenciones de ruta de acceso.
La búsqueda de vistas de una página de Razor incluye la carpeta Pages. Los diseños, plantillas y parciales
que usa con los controladores de MVC y las vistas de Razor convencionales simplemente funcionan.
Agregue un archivo Pages/_ViewImports.cshtml:

@namespace RazorPagesContacts.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

@namespace se explica más adelante en el tutorial. La directiva @addTagHelper pone las aplicaciones
auxiliares de etiquetas integradas en todas las páginas de la carpeta Pages.
Cuando la directiva @namespace se usa explícitamente en una página:

@page
@namespace RazorPagesIntro.Pages.Customers

@model NameSpaceModel

<h2>Name space</h2>
<p>
@Model.Message
</p>

La directiva establece el espacio de nombres de la página. La directiva @model no necesita incluir el


espacio de nombres.
Cuando la directiva @namespace se encuentra en _ViewImports.cshtml, el espacio de nombres
especificado proporciona el prefijo del espacio de nombres generado en la página que importa la
directiva @namespace . El resto del espacio de nombres generado (la parte del sufijo) es la ruta de acceso
relativa separada por puntos entre la carpeta que contiene _ViewImports.cshtml y la carpeta que contiene
la página.
Por ejemplo, la clase PageModel Pages/Customers/Edit.cshtml.cs establece explícitamente el espacio de
nombres:

namespace RazorPagesContacts.Pages
{
public class EditModel : PageModel
{
private readonly AppDbContext _db;

public EditModel(AppDbContext db)


{
_db = db;
}

// Code removed for brevity.

El archivo Pages/_ViewImports.cshtml establece el espacio de nombres siguiente:


@namespace RazorPagesContacts.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

El espacio de nombres generado para la página de Razor Pages/Customers/Edit.cshtml es el mismo que


la clase PageModel .
@namespace también funciona con las vistas de Razor convencionales.
El archivo de vista Pages/Create.cshtml original:

@page
@model RazorPagesContacts.Pages.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<html>
<body>
<p>
Enter your name.
</p>
<div asp-validation-summary="All"></div>
<form method="POST">
<div>Name: <input asp-for="Customer.Name" /></div>
<input type="submit" />
</form>
</body>
</html>

El archivo de vista Pages/Create.cshtml actualizado:

@page
@model CreateModel

<html>
<body>
<p>
Enter your name.
</p>
<div asp-validation-summary="All"></div>
<form method="POST">
<div>Name: <input asp-for="Customer.Name" /></div>
<input type="submit" />
</form>
</body>
</html>

El proyecto de inicio de las páginas de Razor contiene Pages/_ValidationScriptsPartial.cshtml, que enlaza


la validación del lado cliente.

Generación de direcciones URL para las páginas


La página Create , mostrada anteriormente, usa RedirectToPage :
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}

_db.Customers.Add(Customer);
await _db.SaveChangesAsync();
return RedirectToPage("/Index");
}

La aplicación tiene la siguiente estructura de archivos o carpetas:


/Pages
Index.cshtml
/Customers
Create.cshtml
Edit.cshtml
Index.cshtml
Las páginas Pages/Customers/Create.cshtml y Pages/Customers/Edit.cshtml redirigen a
Pages/Index.cshtml si se realiza correctamente. La cadena /Index forma parte del URI para tener acceso
a la página anterior. La cadena /Index puede usarse para generar los URI para la página
Pages/Index.cshtml. Por ejemplo:
Url.Page("/Index", ...)
<a asp-page="/Index">My Index Page</a>
RedirectToPage("/Index")

El nombre de página es la ruta de acceso a la página de la carpeta raíz /Pages, incluido un / inicial, por
ejemplo /Index . Los ejemplos anteriores de generación de URL ofrecen opciones mejoradas y
capacidades funcionales en comparación con la escritura a mano de estas. La generación de direcciones
URL usa el enrutamiento y puede generar y codificar parámetros según cómo se defina la ruta en la ruta
de acceso de destino.
La generación de direcciones URL para las páginas admite nombres relativos. En la siguiente tabla, se
muestra qué página de índice está seleccionada con diferentes parámetros RedirectToPage de
Pages/Customers/Create.cshtml:

REDIRECTTOPAGE(X) PÁGINA

RedirectToPage("/Index") Pages/Index

RedirectToPage("./Index"); Pages/Customers/Index

RedirectToPage("../Index") Pages/Index

RedirectToPage("Index") Pages/Customers/Index

RedirectToPage("Index") , y RedirectToPage("../Index") son nombres


RedirectToPage("./Index")
relativos. El parámetro RedirectToPage se combina con la ruta de acceso de la página actual para
calcular el nombre de la página de destino.
Vincular el nombre relativo es útil al crear sitios con una estructura compleja. Si usa nombres relativos
para vincular entre páginas en una carpeta, puede cambiar el nombre de esa carpeta. Todos los vínculos
seguirán funcionando (porque no incluyen el nombre de carpeta).

Atributo ViewData
Se pueden pasar datos a una página con ViewDataAttribute. Los valores de las propiedades de
controladores o modelos de página de Razor decoradas con [ViewData] se almacenan y cargan desde
ViewDataDictionary.
En el ejemplo siguiente, el valor AboutModel contiene una propiedad Title decorada con [ViewData] .
La propiedad Title se establece en el título de la página Acerca de:

public class AboutModel : PageModel


{
[ViewData]
public string Title { get; } = "About";

public void OnGet()


{
}
}

En la página Acerca de, acceda 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>
...

TempData
ASP.NET Core 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 útil para el redireccionamiento cuando se necesitan los datos de más de una única solicitud.

El atributo [TempData] es nuevo en ASP.NET Core 2.0 y es compatible con controladores y páginas.
El siguiente código establece el valor de Message mediante TempData :
public class CreateDotModel : PageModel
{
private readonly AppDbContext _db;

public CreateDotModel(AppDbContext db)


{
_db = db;
}

[TempData]
public string Message { get; set; }

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

public async Task<IActionResult> OnPostAsync()


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

_db.Customers.Add(Customer);
await _db.SaveChangesAsync();
Message = $"Customer {Customer.Name} added";
return RedirectToPage("./Index");
}
}

El siguiente marcado en el archivo Pages/Customers/Index.cshtml muestra el valor de Message mediante


TempData .

<h3>Msg: @Model.Message</h3>

El modelo de página Pages/Customers/Index.cshtml.cs aplica el atributo [TempData] a la propiedad


Message .

[TempData]
public string Message { get; set; }

Vea TempData para obtener más información.

Varios controladores por página


La siguiente página genera marcado para dos controladores de páginas mediante la aplicación auxiliar
de etiquetas asp-page-handler :
@page
@model CreateFATHModel

<html>
<body>
<p>
Enter your name.
</p>
<div asp-validation-summary="All"></div>
<form method="POST">
<div>Name: <input asp-for="Customer.Name" /></div>
<input type="submit" asp-page-handler="JoinList" value="Join" />
<input type="submit" asp-page-handler="JoinListUC" value="JOIN UC" />
</form>
</body>
</html>

El formulario del ejemplo anterior tiene dos botones de envío, y cada uno de ellos usa
FormActionTagHelper para enviar a una dirección URL diferente. El atributo asp-page-handler es un
complemento de asp-page . asp-page-handler genera direcciones URL que envían a cada uno de los
métodos de controlador definidos por una página. asp-page no se especifica porque el ejemplo se
vincula a la página actual.
Modelo de página:
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesContacts.Data;

namespace RazorPagesContacts.Pages.Customers
{
public class CreateFATHModel : PageModel
{
private readonly AppDbContext _db;

public CreateFATHModel(AppDbContext db)


{
_db = db;
}

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

public async Task<IActionResult> OnPostJoinListAsync()


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

_db.Customers.Add(Customer);
await _db.SaveChangesAsync();
return RedirectToPage("/Index");
}

public async Task<IActionResult> OnPostJoinListUCAsync()


{
if (!ModelState.IsValid)
{
return Page();
}
Customer.Name = Customer.Name?.ToUpper();
return await OnPostJoinListAsync();
}
}
}

El código anterior usa métodos de controlador con nombre. Los métodos de controlador con nombre se
crean tomando el texto en el nombre después de On<HTTP Verb> y antes de Async (si existe). En el
ejemplo anterior, los métodos de página son OnPostJoinListAsync y OnPostJoinListUCAsync.
Quitando OnPost y Async, los nombres de controlador son JoinList y JoinListUC .

<input type="submit" asp-page-handler="JoinList" value="Join" />


<input type="submit" asp-page-handler="JoinListUC" value="JOIN UC" />

Al usar el código anterior, la ruta de dirección URL que envía a OnPostJoinListAsync es


http://localhost:5000/Customers/CreateFATH?handler=JoinList . La ruta de dirección URL que envía a
OnPostJoinListUCAsync es http://localhost:5000/Customers/CreateFATH?handler=JoinListUC .

Rutas personalizadas
Use la directiva @page para:
Especificar una ruta personalizada a una página. Por ejemplo, la ruta a la página Acerca de se puede
establecer en /Some/Other/Path con @page "/Some/Other/Path" .
Anexar segmentos a la ruta predeterminada de una página. Por ejemplo, se puede agregar un
segmento "item" a la ruta predeterminada de una página con @page "item" .
Anexar parámetros a la ruta predeterminada de una página. Por ejemplo, un parámetro de
identificador, id , puede ser necesario para una página con @page "{id}" .
Se admite una ruta de acceso relativa raíz designada por una tilde ( ~ ) al principio de la ruta de acceso.
Por ejemplo, @page "~/Some/Other/Path" es lo mismo que @page "/Some/Other/Path" .
La cadena de consulta ?handler=JoinList de la dirección URL se puede cambiar por un segmento de
ruta /JoinList , para lo cual hay que especificar la plantilla de ruta @page "{handler?}" .
Si no le gusta la cadena de consulta ?handler=JoinList en la dirección URL, puede cambiar la ruta para
poner el nombre del controlador en la parte de la ruta de la dirección URL. Para personalizar la ruta, se
puede agregar una plantilla de ruta entre comillas dobles después de la directiva @page .

@page "{handler?}"
@model CreateRouteModel

<html>
<body>
<p>
Enter your name.
</p>
<div asp-validation-summary="All"></div>
<form method="POST">
<div>Name: <input asp-for="Customer.Name" /></div>
<input type="submit" asp-page-handler="JoinList" value="Join" />
<input type="submit" asp-page-handler="JoinListUC" value="JOIN UC" />
</form>
</body>
</html>

Al usar el código anterior, la ruta de dirección URL que envía a OnPostJoinListAsync es


http://localhost:5000/Customers/CreateFATH/JoinList . La ruta de dirección URL que envía a
OnPostJoinListUCAsync es http://localhost:5000/Customers/CreateFATH/JoinListUC .

El signo ? que sigue a handler significa que el parámetro de ruta es opcional.

Valores de configuración
Para configurar opciones avanzadas, use el método de extensión AddRazorPagesOptions en el generador
de MVC:

public void ConfigureServices(IServiceCollection services)


{
services.AddMvc()
.AddRazorPagesOptions(options =>
{
options.RootDirectory = "/MyPages";
options.Conventions.AuthorizeFolder("/MyPages/Admin");
});
}

Actualmente, puede usar RazorPagesOptions para establecer el directorio raíz de páginas, o agregar
convenciones de modelo de aplicación a las páginas. En el futuro habilitaremos más extensibilidad de
este modo.
Para precompilar vistas, vea Razor view compilation (Compilación de vistas de Razor).
Descargue o vea el código de ejemplo.
Vea Introducción a las páginas de Razor, que se basa en esta introducción.
Especificar que las páginas de Razor se encuentran en la raíz de contenido
De forma predeterminada, la ruta raíz de las páginas de Razor es el directorio /Pages. Agregue
WithRazorPagesAtContentRoot a AddMvc para especificar que las páginas de Razor están en la ruta raíz
de contenido (ContentRootPath) de la aplicación:

services.AddMvc()
.AddRazorPagesOptions(options =>
{
...
})
.WithRazorPagesAtContentRoot();

Especificar que las páginas de Razor se encuentran en un directorio raíz personalizado


Agregue WithRazorPagesRoot a AddMvc para especificar que las páginas de Razor están en un
directorio raíz personalizado en la aplicación (proporcione una ruta de acceso relativa):

services.AddMvc()
.AddRazorPagesOptions(options =>
{
...
})
.WithRazorPagesRoot("/path/to/razor/pages");

Vea también
Introducción a ASP.NET Core
Sintaxis de Razor
Introducción a las páginas de Razor
Convenciones de autorización de las páginas de Razor
Proveedores personalizados de rutas y modelos de página de páginas de Razor
Pruebas unitarias de páginas de Razor
Métodos de filtrado de páginas de Razor en ASP.NET
Core
18/09/2018 • 7 minutes to read • Edit Online

Por Rick Anderson


Los filtros de páginas de Razor IPageFilter e IAsyncPageFilter permiten que las páginas de Razor ejecuten código
antes y después de que se haya ejecutado un controlador de páginas de Razor. Estos filtros son similares a los
filtros de acción de ASP.NET Core MVC, salvo por el hecho de que no se pueden usar con métodos de
controlador de páginas individuales.
Los filtros de páginas de Razor:
Ejecutan código después de que se haya seleccionado un método de controlador, pero antes de que el enlace
de modelos tenga lugar.
Ejecutan código antes de que se ejecute el método de controlador, después de que el enlace de modelos se
haya completado.
Ejecutan código después de que se haya ejecutado el método de controlador.
Se pueden implementar en una página o globalmente.
No se pueden usar con métodos de controlador de páginas específicas.
Se puede ejecutar código antes de que un método de controlador se ejecute por medio del constructor de página
o de middleware, pero solo los filtros de páginas de Razor tienen acceso a HttpContext. Los filtros tienen un
parámetro derivado FilterContext que concede acceso a HttpContext . Por ejemplo, en el ejemplo Implementar un
atributo de filtro se agrega un encabezado a la respuesta, cosa que no es posible con constructores o con
middleware.
Vea o descargue el código de ejemplo (cómo descargarlo)
Los filtros de páginas de Razor proporcionan los siguientes métodos, que se pueden usar globalmente o bien en
el nivel de página:
Métodos sincrónicos:
OnPageHandlerSelected: se llama a este método después de que se haya seleccionado un método de
controlador, pero antes de que el enlace de modelos tenga lugar.
OnPageHandlerExecuting: se llama a este método antes de que se ejecute el método de controlador,
después de que el enlace de modelos se haya completado.
OnPageHandlerExecuted: se llama a este método después de que se ejecute el método de controlador,
antes de obtener el resultado de la acción.
Métodos asincrónicos:
OnPageHandlerSelectionAsync: se llama a este método de forma asincrónica después de que se haya
seleccionado el método de controlador, pero antes de que el enlace de modelos tenga lugar.
OnPageHandlerExecutionAsync: se llama a este método de forma asincrónica antes de que se invoque
el método de controlador, después de que el enlace de modelos se haya completado.
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 implementan ambas interfaces, solo se llamará a los métodos asincrónicos. La misma regla se
cumple con las invalidaciones en páginas: implemente la versión sincrónica o asincrónica de la invalidación, pero no ambas.

Implementar filtros de páginas de Razor globalmente


El siguiente código implementa IAsyncPageFilter :

using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Logging;
using System.Threading.Tasks;

namespace PageFilter.Filters
{
public class SampleAsyncPageFilter : IAsyncPageFilter
{
private readonly ILogger _logger;

public SampleAsyncPageFilter(ILogger logger)


{
_logger = logger;
}

public async Task OnPageHandlerSelectionAsync(


PageHandlerSelectedContext context)
{
_logger.LogDebug("Global OnPageHandlerSelectionAsync called.");
await Task.CompletedTask;
}

public async Task OnPageHandlerExecutionAsync(


PageHandlerExecutingContext context,
PageHandlerExecutionDelegate next)
{
_logger.LogDebug("Global OnPageHandlerExecutionAsync called.");
await next.Invoke();
}
}
}

En el código anterior, ILogger no es necesario; se usa para proporcionar información de seguimiento relativa a la
aplicación.
El siguiente código habilita SampleAsyncPageFilter en la clase Startup :

public void ConfigureServices(IServiceCollection services)


{
services.AddMvc(options =>
{
options.Filters.Add(new SampleAsyncPageFilter(_logger));
});
}

El siguiente código muestra la clase Startup completa:


using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using PageFilter.Filters;

namespace PageFilter
{
public class Startup
{
ILogger _logger;
public Startup(ILoggerFactory loggerFactory, IConfiguration configuration)
{
_logger = loggerFactory.CreateLogger<GlobalFiltersLogger>();
Configuration = configuration;
}

public IConfiguration Configuration { get; }

public void ConfigureServices(IServiceCollection services)


{
services.AddMvc(options =>
{
options.Filters.Add(new SampleAsyncPageFilter(_logger));
});
}

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

El siguiente código llama a AddFolderApplicationModelConvention para aplicar SampleAsyncPageFilter solo a las


páginas en /subFolder:

public void ConfigureServices(IServiceCollection services)


{
services.AddMvc()
.AddRazorPagesOptions(options =>
{
options.Conventions.AddFolderApplicationModelConvention(
"/subFolder",
model => model.Filters.Add(new SampleAsyncPageFilter(_logger)));
});
}

El siguiente código implementa el método sincrónico IPageFilter :


using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Logging;

namespace PageFilter.Filters
{
public class SamplePageFilter : IPageFilter
{
private readonly ILogger _logger;

public SamplePageFilter(ILogger logger)


{
_logger = logger;
}

public void OnPageHandlerSelected(PageHandlerSelectedContext context)


{
_logger.LogDebug("Global sync OnPageHandlerSelected called.");
}

public void OnPageHandlerExecuting(PageHandlerExecutingContext context)


{
_logger.LogDebug("Global sync PageHandlerExecutingContext called.");
}

public void OnPageHandlerExecuted(PageHandlerExecutedContext context)


{
_logger.LogDebug("Global sync OnPageHandlerExecuted called.");
}
}
}

El siguiente código habilita SamplePageFilter :

public void ConfigureServices(IServiceCollection services)


{
services.AddMvc(options =>
{
options.Filters.Add(new SamplePageFilter(_logger));
});
}

Implementar filtros de páginas de Razor invalidando métodos de filtro


El siguiente código invalida los filtros de páginas de Razor sincrónicos:
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;

namespace PageFilter.Pages
{
public class IndexModel : PageModel
{
private readonly ILogger _logger;

public IndexModel(ILogger<IndexModel> logger)


{
_logger = logger;
}
public string Message { get; set; }

public void OnGet()


{
_logger.LogDebug("IndexModel/OnGet");
}

public override void OnPageHandlerSelected(


PageHandlerSelectedContext context)
{
_logger.LogDebug("IndexModel/OnPageHandlerSelected");
}

public override void OnPageHandlerExecuting(


PageHandlerExecutingContext context)
{
Message = "Message set in handler executing";
_logger.LogDebug("IndexModel/OnPageHandlerExecuting");
}

public override void OnPageHandlerExecuted(


PageHandlerExecutedContext context)
{
_logger.LogDebug("IndexModel/OnPageHandlerExecuted");
}
}
}

Implementar un atributo de filtro


El filtro basado en atributos integrado OnResultExecutionAsync puede ser una subclase. El siguiente filtro agrega
un encabezado a la respuesta:
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Filters;

namespace PageFilter.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 });
}
}
}

El siguiente código se aplica al atributo AddHeader :

[AddHeader("Author", "Rick")]
public class ContactModel : PageModel
{
private readonly ILogger _logger;

public ContactModel(ILogger<ContactModel> logger)


{
_logger = logger;
}
public string Message { get; set; }

public async Task OnGetAsync()


{
Message = "Your contact page.";
_logger.LogDebug("Contact/OnGet");
await Task.CompletedTask;
}
}

Vea Invalidación del orden predeterminado para obtener instrucciones sobre cómo invalidar el orden.
Vea Cancelación y cortocircuito para obtener instrucciones sobre cómo cortocircuitar la canalización de filtro de
un filtro.

Atributo de filtro Authorize


El atributo Authorize se puede aplicar a un PageModel :
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace PageFilter.Pages
{
[Authorize]
public class ModelWithAuthFilterModel : PageModel
{
public IActionResult OnGet() => Page();
}
}
Crear la interfaz de usuario reutilizable con el
proyecto de biblioteca de clases de Razor en
ASP.NET Core
21/09/2018 • 10 minutes to read • Edit Online

Por Rick Anderson


Las vistas, las páginas, los controladores, los modelos de página, los modelos de datos y los componentes de
vista de Razor se pueden integrar en una biblioteca de clases de Razor (RCL ). Las RCL se pueden empaquetar y
reutilizar. Las aplicaciones pueden incluir la RCL y reemplazar las vistas y páginas que contienen. Cuando existe
una vista, vista parcial o página de Razor tanto en la aplicación web como en la RCL, tendrá prioridad el marcado
de Razor (archivo .cshtml) de la aplicación web.
Esta característica requiere SDK de .NET Core 2.1 o versiones posteriores
Vea o descargue el código de ejemplo (cómo descargarlo)

Crear una biblioteca de clases que contenga interfaz de usuario de


Razor
Visual Studio
CLI de .NET Core
En el menú Archivo de Visual Studio, seleccione Nuevo > Proyecto.
Seleccione Aplicación web de ASP.NET Core.
Asigne un nombre a la biblioteca (por ejemplo, "RazorClassLib") > Aceptar. Para evitar un conflicto de
nombres de archivo con la biblioteca de vistas generada, asegúrese de que el nombre de la biblioteca no
acaba en .Views .
Confirme que ASP.NET Core 2.1 o una versión posterior está seleccionado.
Seleccione Razor Class Library (Biblioteca de clases de Razor) > Aceptar.
Una biblioteca de clases de Razor tiene el siguiente archivo del proyecto:

<Project Sdk="Microsoft.NET.Sdk.Razor">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.1.2" />
</ItemGroup>

</Project>

Agregue archivos de Razor a la RCL.


Las plantillas de ASP.NET Core se suponen que el contenido RCL está en el áreas carpeta. Consulte diseño de
páginas RCL para crear contenido en un RCL que expone ~/Pages lugar ~/Areas/Pages .
Hacer referencia a contenido de la biblioteca de clases de Razor
Los siguientes elementos pueden hacer referencia a la RCL:
Paquete NuGet. Vea Creación de paquetes NuGet, dotnet add package y Creación y publicación de un
paquete NuGet.
{NombreDeProyecto }.csproj. Vea dotnet-add reference.

Tutorial: Crear un proyecto de biblioteca de clases de Razor y usar


desde un proyecto de páginas de Razor
En lugar de crearlo, puede descargar el proyecto completo y comprobarlo. La descarga de ejemplo contiene más
código y vínculos que hacen que el proyecto sea fácil de comprobar. Puede dejar sus comentarios en este
problema de GitHub sobre las descargas de ejemplo en comparación con las instrucciones paso a paso.
Comprobar la aplicación de descarga
Si no ha descargado la aplicación final y prefiere crear el proyecto de tutorial, omita este paso y vaya a la
siguiente sección.
Visual Studio
CLI de .NET Core
Abra el archivo .sln en Visual Studio. Ejecute la aplicación.
Siga las instrucciones de Probar WebApp1.

Crear una biblioteca de clases de Razor


En esta sección, crearemos una biblioteca de clases de Razor (RCL ) y se agregarán a ella archivos de Razor.
Visual Studio
CLI de .NET Core
Cree el proyecto de RCL:
En el menú Archivo de Visual Studio, seleccione Nuevo > Proyecto.
Seleccione Aplicación web de ASP.NET Core.
Nombre de la aplicación RazorUIClassLib > Aceptar.
Confirme que ASP.NET Core 2.1 o una versión posterior está seleccionado.
Seleccione Razor Class Library (Biblioteca de clases de Razor) > Aceptar.
Agregue un archivo de vista parcial de Razor denominado
RazorUIClassLib/Areas/MyFeature/Pages/Shared/_Message.cshtml.
Agregar archivos de Razor y carpetas al proyecto
Reemplace el marcado de RazorUIClassLib/Areas/MyFeature/Pages/Shared/_Message.cshtml por el
siguiente código:

<h3> _Message.cshtml partial view.</h3>

<p>RazorUIClassLib\Areas\MyFeature\Pages\Shared\_Message.cshtml</p>

Reemplace el marcado de RazorUIClassLib/Areas/MyFeature/Pages/Page1.cshtml por el siguiente código:


@page
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<h2>Hello from a Razor UI class library!</h2>


<p> From RazorUIClassLib\Areas\MyFeature\Pages\Page1.cshtml</p>

<partial name="_Message" />

Se necesita @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers para usar la vista parcial (


<partial name="_Message" /> ). En lugar de incluir la directiva @addTagHelper , puede agregar un archivo
_ViewImports.cshtml. Por ejemplo:

dotnet new viewimports -o RazorUIClassLib/Areas/MyFeature/Pages

Para obtener más información sobre _ViewImports.cshtml, consulte importar directivas compartidas
Compile la biblioteca de clases para confirmar que no hay ningún error de compilador:

dotnet build RazorUIClassLib

El resultado de la compilación contiene RazorUIClassLib.dll y RazorUIClassLib.Views.dll.


RazorUIClassLib.Views.dll incluye el contenido de Razor compilado.
Usar la biblioteca de interfaz de usuario de Razor desde un proyecto de páginas de Razor
Visual Studio
CLI de .NET Core
Cree la aplicación web de páginas de Razor:
En el Explorador de soluciones, haga clic con el botón derecho en la solución > Agregar > Nuevo
proyecto.
Seleccione Aplicación web de ASP.NET Core.
Denomine la aplicación WebApp1.
Confirme que ASP.NET Core 2.1 o una versión posterior está seleccionado.
Seleccione Aplicación web > Aceptar.
En el Explorador de soluciones, haga clic con el botón derecho en WebApp1 y seleccione Establecer
como proyecto de inicio.
En el Explorador de soluciones, haga clic con el botón derecho en WebApp1 y seleccione
Dependencias de compilación > Dependencias del proyecto.
Marque RazorUIClassLib como dependencia de WebApp1.
En el Explorador de soluciones, haga clic con el botón derecho en WebApp1 y seleccione Agregar >
Referencia.
En el cuadro de diálogo Administrador de referencias, marque RazorUIClassLib > Aceptar.
Ejecute la aplicación.
Probar WebApp1
Compruebe si la biblioteca de clases de interfaz de usuario de Razor se está usando.
Vaya a /MyFeature/Page1 .

Reemplazar vistas, vistas parciales y páginas


Cuando existe una vista, vista parcial o página de Razor tanto en la aplicación web como en la biblioteca de
clases de Razor, tendrá prioridad el marcado de Razor (archivo .cshtml) de la aplicación web. Por ejemplo,
agregar WebApp1/Areas/MyFeature/Pages/Page1.cshtml a WebApp1, y Page1 en la WebApp1 tendrá
prioridad sobre Page1 en la biblioteca de clases de Razor.
En la descarga de ejemplo, cambie el nombre WebApp1/Areas/MyFeature2 por WebApp1/Areas/MyFeature
para comprobar la prioridad.
Copie la vista parcial RazorUIClassLib/Areas/MyFeature/Pages/Shared/_Message.cshtml en
WebApp1/Areas/MyFeature/Pages/Shared/_Message.cshtml. Actualice el marcado para señalar la nueva
ubicación. Compile y ejecute la aplicación para comprobar si se está usando la versión de la vista parcial de la
aplicación.
Diseño de páginas RCL
Para hacer referencia RCL contenido como si formara parte de la aplicación web a páginas carpeta, cree el
proyecto RCL con la siguiente estructura de archivos:
Páginas/RazorUIClassLib
RazorUIClassLib/páginas/Shared
Supongamos que RazorUIClassLib, compartidos o páginas contiene dos archivos parciales: _Header.cshtml y
_Footer.cshtml. El <partial> se puede agregar etiquetas a _Layout.cshtml archivo:

<body>
<partial name="_Header">
@RenderBody()
<partial name="_Footer">
</body>
Convenciones de aplicación y de ruta de páginas de
Razor en ASP.NET Core
18/09/2018 • 33 minutes to read • Edit Online

Por Luke Latham


Aquí encontrará información para saber cómo usar las convenciones de proveedor de modelos de aplicación y de
ruta con objeto de controlar el enrutamiento, la detección y el procesamiento en aplicaciones de páginas de Razor.
Si necesita configurar rutas de una página personalizadas para páginas concretas, configure el enrutamiento a
esas páginas con la convención AddPageRoute, descrita más adelante en este tema.
Para especificar una ruta de página, agregue segmentos de ruta o agregar parámetros a una ruta, use la página
@page directiva. Para obtener más información, consulte rutas personalizadas.

Hay palabras reservadas que no se puede usar como segmentos de ruta o nombres de parámetro. Para obtener
más información, consulte enrutamiento: enrutamientos nombres reservados.
Vea o descargue el código de ejemplo (cómo descargarlo)

ESCENARIO EL EJEMPLO EXPLICA CÓMO...

Convenciones de modelo Agregar un encabezado y una plantilla de ruta a las páginas


de una aplicación.
Conventions.Add
IPageRouteModelConvention
IPageApplicationModelConvention

Convenciones de acción de ruta de página Agregar una plantilla de ruta a las páginas de una carpeta y a
AddFolderRouteModelConvention una sola página.
AddPageRouteModelConvention
AddPageRoute

Convenciones de acción del modelo de página Agregar un encabezado a las páginas de una carpeta, agregar
AddFolderApplicationModelConvention un encabezado a una sola página y configurar una fábrica de
AddPageApplicationModelConvention filtros para agregar un encabezado a las páginas de la
ConfigureFilter (clase de filtro, expresión lambda o aplicación.
fábrica de filtros)

Proveedor de modelos de aplicación de página Reemplazar el proveedor de modelos de página


predeterminado predeterminado para cambiar las convenciones de nombres
de controlador.
ESCENARIO EL EJEMPLO EXPLICA CÓMO...

Convenciones de modelo Agregar un encabezado y una plantilla de ruta a las páginas


de una aplicación.
Conventions.Add
IPageRouteModelConvention
IPageApplicationModelConvention
IPageHandlerModelConvention

Convenciones de acción de ruta de página Agregar una plantilla de ruta a las páginas de una carpeta y a
AddFolderRouteModelConvention una sola página.
AddPageRouteModelConvention
AddPageRoute

Convenciones de acción del modelo de página Agregar un encabezado a las páginas de una carpeta, agregar
AddFolderApplicationModelConvention un encabezado a una sola página y configurar una fábrica de
AddPageApplicationModelConvention filtros para agregar un encabezado a las páginas de la
ConfigureFilter (clase de filtro, expresión lambda o aplicación.
fábrica de filtros)

Proveedor de modelos de aplicación de página Reemplazar el proveedor de modelos de página


predeterminado predeterminado para cambiar las convenciones de nombres
de controlador.

Las convenciones de páginas de Razor se agregan y configuran a través del método de extensión
AddRazorPagesOptions a AddMvc en la colección de servicio de la clase Startup . Más avanzado el tema veremos
los siguientes ejemplos de convención:

public void ConfigureServices(IServiceCollection services)


{
services.AddMvc()
.AddRazorPagesOptions(options =>
{
options.Conventions.Add( ... );
options.Conventions.AddFolderRouteModelConvention("/OtherPages", model => { ... });
options.Conventions.AddPageRouteModelConvention("/About", model => { ... });
options.Conventions.AddPageRoute("/Contact", "TheContactPage/{text?}");
options.Conventions.AddFolderApplicationModelConvention("/OtherPages", model => { ... });
options.Conventions.AddPageApplicationModelConvention("/About", model => { ... });
options.Conventions.ConfigureFilter(model => { ... });
options.Conventions.ConfigureFilter( ... );
});
}

Orden de la ruta
Rutas especifican una Order para procesar (coincidencia de ruta).

ORDEN COMPORTAMIENTO

-1 La ruta se procesa antes de otras rutas se procesan.

0 No se especifica el orden (valor predeterminado). No asignar


Order ( Order = null ) el valor predeterminado es la ruta
Order en 0 (cero) para su procesamiento.
ORDEN COMPORTAMIENTO

1, 2, … n Especifica el orden de procesamiento de la ruta.

Procesamiento de la ruta se establece mediante la convención:


Las rutas se procesan en orden secuencial (-1, 0, 1, 2, … n).
Cuando las rutas tienen el mismo Order , más ruta específica se compara primero, seguido por rutas menos
específicas.
Cuando las rutas con el mismo Order y el mismo número de parámetros coincide con una dirección URL de
solicitud, las rutas se procesan en el orden en que se agreguen a la PageConventionCollection.
Si es posible, evite según un orden de procesamiento de la ruta establecida. Por lo general, selecciona la ruta
correcta con coincidencia de dirección URL de enrutamiento. Si se debe establecer ruta Order propiedades para
enrutar las solicitudes correctamente, esquema de enrutamiento de la aplicación es probablemente confuso para
los clientes y frágil mantener. Buscar simplificar el esquema de enrutamiento de la aplicación. La aplicación de
ejemplo requiere una ruta explicita de procesamiento de orden en que se muestran varios escenarios de
enrutamientos mediante una sola aplicación. Sin embargo, debe intentar evitar la práctica de la ruta de
configuración Order en aplicaciones de producción.
Enrutamiento de las páginas de Razor y compartir de enrutamiento de controlador MVC una implementación.
Información sobre el orden de la ruta en los temas MVC está disponible en enrutamiento a acciones del
controlador: orden de las rutas de atributo.

Convenciones de modelo
Agregue un delegado de IPageConvention con el fin de agregar convenciones de modelo y aplicarlas a páginas de
Razor.
Agregar una convención de modelo de ruta a todas las páginas
Use Conventions para crear y agregar un IPageRouteModelConvention a la colección de instancias
IPageConvention que se van a aplicar durante la construcción del modelo de ruta de página.
En la aplicación de ejemplo se agrega una plantilla de ruta {globalTemplate?} a todas las páginas de la aplicación:

public class GlobalTemplatePageRouteModelConvention


: IPageRouteModelConvention
{
public void Apply(PageRouteModel model)
{
var selectorCount = model.Selectors.Count;
for (var i = 0; i < selectorCount; i++)
{
var selector = model.Selectors[i];
model.Selectors.Add(new SelectorModel
{
AttributeRouteModel = new AttributeRouteModel
{
Order = 1,
Template = AttributeRouteModel.CombineTemplates(
selector.AttributeRouteModel.Template,
"{globalTemplate?}"),
}
});
}
}
}
La propiedad Order de AttributeRouteModel está establecida en 1 . Esto garantiza que la ruta siguiente que
coincida con el comportamiento de la aplicación de ejemplo:
Una plantilla de ruta para TheContactPage/{text?} se agrega más adelante en el tema. La ruta de la página
Contact tiene un orden predeterminado de null ( Order = 0 ), para que coincida con antes el
{globalTemplate?} plantilla de ruta.
Un {aboutTemplate?} plantilla de ruta se agrega más adelante en el tema. La plantilla {aboutTemplate?} tiene
un Order con un valor de 2 . Cuando la página About se solicita en /About/RouteDataValue , "RouteDataValue"
se carga en RouteData.Values["globalTemplate"] ( Order = 1 ) y no RouteData.Values["aboutTemplate"] (
Order = 2 ), debido a la configuración de la propiedad Order .
Un {otherPagesTemplate?} plantilla de ruta se agrega más adelante en el tema. La plantilla
{otherPagesTemplate?} tiene un Order con un valor de 2 . Cuando cualquier página en el OtherPages/páginas
carpeta se solicita con un parámetro de ruta (por ejemplo, /OtherPages/Page1/RouteDataValue ),
"RouteDataValue" se carga en RouteData.Values["globalTemplate"] ( Order = 1 ) y no
RouteData.Values["otherPagesTemplate"] ( Order = 2 ) debido a la configuración del Order propiedad.

Siempre que sea posible, no establezca la Order , que da como resultado Order = 0 . Se basan en enrutamiento
para seleccionar la ruta correcta.
Las opciones de páginas de Razor (como, por ejemplo, agregar Conventions) se agregan cuando MVC se agrega a
la colección de servicio en Startup.ConfigureServices . Para obtener un ejemplo, vea la aplicación de ejemplo.

options.Conventions.Add(new GlobalTemplatePageRouteModelConvention());

Solicite la página About del ejemplo en localhost:5000/About/GlobalRouteValue y revise el resultado:

Agregar una convención de modelo de aplicación a todas las páginas


Use Conventions para crear y agregar un IPageApplicationModelConvention a la colección de instancias
IPageConvention que se van a aplicar durante la construcción del modelo de aplicación de página.
Para demostrar esta y otras convenciones más adelante en este tema, la aplicación de ejemplo incluye una clase
AddHeaderAttribute . El constructor de esta clase acepta una cadena name y una matriz de cadenas values . Estos
valores se usan en su método OnResultExecuting para establecer un encabezado de respuesta. La clase completa
se muestra en la sección Convenciones de acción del modelo de página más adelante en este tema.
En la aplicación de ejemplo se usa la clase AddHeaderAttribute para agregar un encabezado ( GlobalHeader ) a
todas las páginas de la aplicación:
public class GlobalHeaderPageApplicationModelConvention
: IPageApplicationModelConvention
{
public void Apply(PageApplicationModel model)
{
model.Filters.Add(new AddHeaderAttribute(
"GlobalHeader", new string[] { "Global Header Value" }));
}
}

Startup.cs:

options.Conventions.Add(new GlobalHeaderPageApplicationModelConvention());

Solicite la página About del ejemplo en localhost:5000/About y revise los encabezados para ver el resultado:

Agregar una convención de modelo de controlador a todas las páginas


Use Conventions para crear y agregar un IPageHandlerModelConvention a la colección de instancias
IPageConvention que se van a aplicar durante la construcción del modelo de controlador de página.

public class GlobalPageHandlerModelConvention


: IPageHandlerModelConvention
{
public void Apply(PageHandlerModel model)
{
...
}
}

Startup.ConfigureServices :

services.AddMvc()
.AddRazorPagesOptions(options =>
{
options.Conventions.Add(new GlobalPageHandlerModelConvention());
});

Convenciones de acción de ruta de página


El proveedor de modelos de ruta predeterminado que se deriva de IPageRouteModelProvider invoca
convenciones diseñadas para proporcionar puntos de extensibilidad que permitan configurar rutas de página.
Convención de modelo de ruta de carpeta
Use AddFolderRouteModelConvention para crear y agregar un IPageRouteModelConvention que invoque una
acción en el modelo PageRouteModel de todas las páginas contenidas en la carpeta especificada.
En la aplicación de ejemplo se usa AddFolderRouteModelConvention para agregar una plantilla de ruta
{otherPagesTemplate?} a las páginas de la carpeta OtherPages:

options.Conventions.AddFolderRouteModelConvention("/OtherPages", model =>


{
var selectorCount = model.Selectors.Count;
for (var i = 0; i < selectorCount; i++)
{
var selector = model.Selectors[i];
model.Selectors.Add(new SelectorModel
{
AttributeRouteModel = new AttributeRouteModel
{
Order = 2,
Template = AttributeRouteModel.CombineTemplates(
selector.AttributeRouteModel.Template,
"{otherPagesTemplate?}"),
}
});
}
});

La propiedad Order de AttributeRouteModel está establecida en 2 . Esto garantiza que la plantilla para
{globalTemplate?} (establecido anteriormente en este tema para 1 ) tiene prioridad para la posición de valor de
datos de la primera ruta cuando se proporciona un solo valor de ruta. Si una página en el OtherPages/páginas
carpeta se solicita con un valor de parámetro de ruta (por ejemplo, /OtherPages/Page1/RouteDataValue ),
"RouteDataValue" se carga en RouteData.Values["globalTemplate"] ( Order = 1 ) y no
RouteData.Values["otherPagesTemplate"] ( Order = 2 ) debido a la configuración del Order propiedad.

Siempre que sea posible, no establezca la Order , que da como resultado Order = 0 . Se basan en enrutamiento
para seleccionar la ruta correcta.
Solicite la página Page1 del ejemplo en localhost:5000/OtherPages/Page1/GlobalRouteValue/OtherPagesRouteValue y
revise el resultado:

Convención de modelo de ruta de página


Use AddPageRouteModelConvention para crear y agregar un IPageRouteModelConvention que invoque una
acción en el modelo PageRouteModel de la página con el nombre especificado.
En la aplicación de ejemplo se usa AddPageRouteModelConvention para agregar una plantilla de ruta
{aboutTemplate?} a la página About:
options.Conventions.AddPageRouteModelConvention("/About", model =>
{
var selectorCount = model.Selectors.Count;
for (var i = 0; i < selectorCount; i++)
{
var selector = model.Selectors[i];
model.Selectors.Add(new SelectorModel
{
AttributeRouteModel = new AttributeRouteModel
{
Order = 2,
Template = AttributeRouteModel.CombineTemplates(
selector.AttributeRouteModel.Template,
"{aboutTemplate?}"),
}
});
}
});

La propiedad Order de AttributeRouteModel está establecida en 2 . Esto garantiza que la plantilla para
{globalTemplate?} (establecido anteriormente en este tema para 1 ) tiene prioridad para la posición de valor de
datos de la primera ruta cuando se proporciona un solo valor de ruta. Si la página About se solicita con un valor
de parámetro de ruta en /About/RouteDataValue , "RouteDataValue" se carga en
RouteData.Values["globalTemplate"] ( Order = 1 ) y no RouteData.Values["aboutTemplate"] ( Order = 2 ) debido a la
configuración del Order propiedad.
Siempre que sea posible, no establezca la Order , que da como resultado Order = 0 . Se basan en enrutamiento
para seleccionar la ruta correcta.
Solicite la página About del ejemplo en localhost:5000/About/GlobalRouteValue/AboutRouteValue y revise el
resultado:

Configurar una ruta de página


Use AddPageRoute para configurar una ruta a una página en la ruta de acceso de página especificada. Los
vínculos generados que apuntan a esa página usan la ruta especificada. AddPageRoute usa
AddPageRouteModelConvention para establecer la ruta.

En la aplicación de ejemplo se crea una ruta a /TheContactPage para Contact.cshtml:


options.Conventions.AddPageRoute("/Contact", "TheContactPage/{text?}");

A la página Contact también se puede llegar en /Contact a través de su ruta predeterminada.


La ruta personalizada de la aplicación de ejemplo que lleva a la página Contact tiene cabida para un segmento de
ruta text opcional ( {text?} ). La página también incluye este segmento opcional en su directiva @page por si el
usuario que la visita tiene acceso a la página en su ruta /Contact :

@page "{text?}"
@model ContactModel
@{
ViewData["Title"] = "Contact";
}

<h1>@ViewData["Title"]</h1>
<h2>@Model.Message</h2>

<address>
One Microsoft Way<br>
Redmond, WA 98052-6399<br>
<abbr title="Phone">P:</abbr>
425.555.0100
</address>

<address>
<strong>Support:</strong> <a href="mailto:Support@example.com">Support@example.com</a><br>
<strong>Marketing:</strong> <a href="mailto:Marketing@example.com">Marketing@example.com</a>
</address>

<p>@Model.RouteDataTextTemplateValue</p>

Observe que la dirección URL generada para el vínculo Contact en la página presentada refleja la ruta
actualizada:

Visite la página Contact a través de su ruta normal, /Contact , o de la ruta personalizada, /TheContactPage . Si se
proporciona un segmento de ruta text adicional, la página muestra el segmento codificado en HTML que se ha
proporcionado:
Convenciones de acción del modelo de página
El proveedor de modelos de página predeterminado que implementa IPageApplicationModelProvider invoca
convenciones que están diseñadas para proporcionar puntos de extensibilidad que permitan configurar modelos
de página. Estas convenciones son útiles al crear y modificar escenarios de detección y procesamiento de páginas.
En los ejemplos de esta sección, la aplicación de ejemplo usa una clase AddHeaderAttribute (que es un atributo
ResultFilterAttribute) que se aplica un encabezado de respuesta:

public class AddHeaderAttribute : ResultFilterAttribute


{
private readonly string _name;
private readonly string[] _values;

public AddHeaderAttribute(string name, string[] values)


{
_name = name;
_values = values;
}

public override void OnResultExecuting(ResultExecutingContext context)


{
context.HttpContext.Response.Headers.Add(_name, _values);
base.OnResultExecuting(context);
}
}

Si empleamos convenciones, el ejemplo indica cómo aplicar el atributo a todas las páginas de una carpeta y a una
sola página.
Convención de modelo de aplicación de carpeta
Use AddFolderApplicationModelConvention para crear y agregar un IPageApplicationModelConvention que
invoque una acción en las instancias de PageApplicationModel de todas las páginas contenidas en la carpeta
especificada.
En el ejemplo se demuestra el uso de AddFolderApplicationModelConvention agregando un encabezado (
OtherPagesHeader ) a las páginas de la carpeta OtherPages de la aplicación:

options.Conventions.AddFolderApplicationModelConvention("/OtherPages", model =>


{
model.Filters.Add(new AddHeaderAttribute(
"OtherPagesHeader", new string[] { "OtherPages Header Value" }));
});

Solicite la página Page1 del ejemplo en localhost:5000/OtherPages/Page1 y revise los encabezados para ver el
resultado:

Convención de modelo de aplicación de página


Use AddPageApplicationModelConvention para crear y agregar un IPageApplicationModelConvention que
invoque una acción en el PageApplicationModel para la página con el nombre especificado.
En el ejemplo se demuestra el uso de AddPageApplicationModelConvention agregando un encabezado ( AboutHeader
) a la página About:

options.Conventions.AddPageApplicationModelConvention("/About", model =>


{
model.Filters.Add(new AddHeaderAttribute(
"AboutHeader", new string[] { "About Header Value" }));
});

Solicite la página About del ejemplo en localhost:5000/About y revise los encabezados para ver el resultado:

Configurar un filtro
ConfigureFilter permite configurar el filtro que se quiere aplicar. Se puede implementar una clase de filtro, pero en
la aplicación de ejemplo se muestra cómo implementar un filtro en una expresión lambda, cosa que sucede en
segundo plano como una fábrica que devuelve un filtro:
options.Conventions.ConfigureFilter(model =>
{
if (model.RelativePath.Contains("OtherPages/Page2"))
{
return new AddHeaderAttribute(
"OtherPagesPage2Header",
new string[] { "OtherPages/Page2 Header Value" });
}
return new EmptyFilter();
});

El modelo de páginas de aplicación se usa para comprobar la ruta de acceso relativa de los segmentos que
conducen a la página Page2 en la carpeta OtherPages. Si la condición se supera, se agrega un encabezado. Si no,
se aplica EmptyFilter .
EmptyFilter es un filtro de acciones. Las páginas de Razor pasan por alto los filtros de acción, así que
EmptyFilter no funciona del modo previsto si la ruta de acceso no contiene OtherPages/Page2 .
Solicite la página Page2 del ejemplo en localhost:5000/OtherPages/Page2 y revise los encabezados para ver el
resultado:

Configurar una fábrica de filtros


ConfigureFilter configura la fábrica especificada para aplicar filtros a todas las páginas de Razor.
En la aplicación de ejemplo se proporciona un ejemplo del uso de una fábrica de filtros, para lo cual se agrega un
encabezado ( FilterFactoryHeader ) con dos valores a las páginas de la aplicación:

options.Conventions.ConfigureFilter(new AddHeaderWithFactory());

AddHeaderWithFactory.cs:
public class AddHeaderWithFactory : IFilterFactory
{
// Implement IFilterFactory
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
{
return new AddHeaderFilter();
}

private class AddHeaderFilter : IResultFilter


{
public void OnResultExecuting(ResultExecutingContext context)
{
context.HttpContext.Response.Headers.Add(
"FilterFactoryHeader",
new string[]
{
"Filter Factory Header Value 1",
"Filter Factory Header Value 2"
});
}

public void OnResultExecuted(ResultExecutedContext context)


{
}
}

public bool IsReusable


{
get
{
return false;
}
}
}

Solicite la página About del ejemplo en localhost:5000/About y revise los encabezados para ver el resultado:

Reemplazar el proveedor de modelo de aplicación de página


predeterminado
Las páginas de Razor usan la interfaz IPageApplicationModelProvider para crear un
DefaultPageApplicationModelProvider. Puede hacer uso del proveedor de modelo predeterminado para
proporcionar su propia lógica de implementación de detección y procesamiento de controladores. La
implementación predeterminada (origen de referencia) establece convenciones para la nomenclatura de métodos
de control sin nombre y con nombre, descrita más abajo.
Métodos de control sin nombre predeterminados
Los métodos de control de verbos HTTP (métodos de controlador "sin nombre") siguen la convención:
On<HTTP verb>[Async] (anexar Async es opcional, pero recomendable en los métodos asincrónicos).
MÉTODO DE CONTROL SIN NOMBRE OPERACIÓN

OnGet / OnGetAsync Inicializar el estado de la página

OnPost / OnPostAsync Controlar las solicitudes POST

OnDelete / OnDeleteAsync Controlar las solicitudes DELETE†

OnPut / OnPutAsync Controlar las solicitudes PUT†

OnPatch / OnPatchAsync Controlar las solicitudes PATCH†

†Usadas para realizar llamadas API a la página.


Métodos de control con nombre predeterminados
Los métodos de control proporcionados por el desarrollador (métodos de control "con nombre") siguen una
convención similar. El nombre del método de control aparece tras el verbo HTTP o entre el verbo HTTP y Async :
On<HTTP verb><handler name>[Async] (anexar Async es opcional, pero recomendable en los métodos asincrónicos).
Por ejemplo, los métodos que procesan mensajes pueden asimilar la nomenclatura se muestra en la siguiente
tabla.

EJEMPLO DE MÉTODO DE CONTROL CON NOMBRE OPERACIÓN DE EJEMPLO

OnGetMessage / OnGetMessageAsync Obtener un mensaje

OnPostMessage / OnPostMessageAsync Publicar un mensaje (POST)

OnDeleteMessage / OnDeleteMessageAsync Eliminar un mensaje (DELETE)†

OnPutMessage / OnPutMessageAsync Colocar un mensaje (PUT)†

OnPatchMessage / OnPatchMessageAsync Aplicar una revisión a un mensaje (PATCH)#8224;

†Usadas para realizar llamadas API a la página.


Personalizar los nombres de los métodos de control
Imaginemos que quiere cambiar la forma en que se denominan los métodos de control con y sin nombre. Un
esquema de nomenclatura alternativo consiste en evitar que los nombres de método comiencen por "On" y usar el
primer segmento de palabra para establecer el verbo HTTP. Puede realizar otros cambios, como convertir los
verbos DELETE, PUT y PATCH en POST. Un esquema de este tipo proporcionaría los nombres de método
mostrados en la siguiente tabla.

MÉTODO DE CONTROL OPERACIÓN

Get Inicializar el estado de la página

Post / PostAsync Controlar las solicitudes POST

Delete / DeleteAsync Controlar las solicitudes DELETE†

Put / PutAsync Controlar las solicitudes PUT†


MÉTODO DE CONTROL OPERACIÓN

Patch / PatchAsync Controlar las solicitudes PATCH†

GetMessage Obtener un mensaje

PostMessage / PostMessageAsync Publicar un mensaje (POST)

DeleteMessage / DeleteMessageAsync Publicar (POST) un mensaje para eliminarlo

PutMessage / PutMessageAsync Publicar (POST) un mensaje para colocarlo

PatchMessage / PatchMessageAsync Publicar (POST) un mensaje para aplicarle una revisión

†Usadas para realizar llamadas API a la página.


Para establecer este esquema, haga uso de la clase DefaultPageApplicationModelProvider e invalide el método
CreateHandlerModel para proporcionar lógica personalizada que resuelva nombres de controlador de
PageModel. En la aplicación de ejemplo se muestra cómo llevar esto a cabo en su propia clase
CustomPageApplicationModelProvider :

public class CustomPageApplicationModelProvider :


DefaultPageApplicationModelProvider
{
public CustomPageApplicationModelProvider(IModelMetadataProvider modelMetadataProvider,
IOptions<MvcOptions> options)
: base (modelMetadataProvider, options)
{
}

protected override PageHandlerModel CreateHandlerModel(MethodInfo method)


{
if (method == null)
{
throw new ArgumentNullException(nameof(method));
}

if (!IsHandler(method))
{
return null;
}

if (!TryParseHandlerMethod(
method.Name, out var httpMethod, out var handlerName))
{
return null;
}

var handlerModel = new PageHandlerModel(


method,
method.GetCustomAttributes(inherit: true))
{
Name = method.Name,
HandlerName = handlerName,
HttpMethod = httpMethod,
};

var methodParameters = handlerModel.MethodInfo.GetParameters();

for (var i = 0; i < methodParameters.Length; i++)


{
var parameter = methodParameters[i];
var parameter = methodParameters[i];
var parameterModel = CreateParameterModel(parameter);
parameterModel.Handler = handlerModel;

handlerModel.Parameters.Add(parameterModel);
}

return handlerModel;
}

private static bool TryParseHandlerMethod(


string methodName, out string httpMethod, out string handler)
{
httpMethod = null;
handler = null;

// Parse the method name according to our conventions to


// determine the required HTTP verb and optional
// handler name.
//
// Valid names look like:
// - Get
// - Post
// - PostAsync
// - GetMessage
// - PostMessage
// - DeleteMessage
// - DeleteMessageAsync

var length = methodName.Length;


if (methodName.EndsWith("Async", StringComparison.Ordinal))
{
length -= "Async".Length;
}

if (length == 0)
{
// The method is named "Async". Exit processing.
return false;
}

// The HTTP verb is at the start of the method name. Use


// casing to determine where it ends.
var handlerNameStart = 1;
for (; handlerNameStart < length; handlerNameStart++)
{
if (char.IsUpper(methodName[handlerNameStart]))
{
break;
}
}

httpMethod = methodName.Substring(0, handlerNameStart);

// The handler name follows the HTTP verb and is optional.


// It includes everything up to the end excluding the
// "Async" suffix, if present.
handler = handlerNameStart == length ? null : methodName.Substring(0, length);

if (string.Equals(httpMethod, "GET", StringComparison.OrdinalIgnoreCase) ||


string.Equals(httpMethod, "POST", StringComparison.OrdinalIgnoreCase))
{
// Do nothing. The httpMethod is correct for GET and POST.
return true;
}
if (string.Equals(httpMethod, "DELETE", StringComparison.OrdinalIgnoreCase) ||
string.Equals(httpMethod, "PUT", StringComparison.OrdinalIgnoreCase) ||
string.Equals(httpMethod, "PATCH", StringComparison.OrdinalIgnoreCase))
{
// Convert HTTP verbs for DELETE, PUT, and PATCH to POST
// Convert HTTP verbs for DELETE, PUT, and PATCH to POST
// For example: DeleteMessage, PutMessage, PatchMessage -> POST
httpMethod = "POST";
return true;
}
else
{
return false;
}
}
}

Esta clase de caracteriza por lo siguiente:


Hereda de DefaultPageApplicationModelProvider.
TryParseHandlerMethod procesa un método de control para averiguar el verbo HTTP ( httpMethod ) y el nombre
del método de control con nombre ( handlerName ) al crear la PageHandlerModel .
Si lo hay, el postfijo Async se omite.
Se distingue entre mayúsculas y minúsculas al analizar el verbo HTTP desde el nombre del método.
Cuando el nombre del método (sin Async ) es igual que el nombre del verbo HTTP, no hay ningún
método de control con nombre. handlerName está establecido en null y el nombre del método es Get ,
Post , Delete , Put o Patch .
Cuando el nombre del método (sin Async ) es más largo que el nombre del verbo HTTP, sí hay un
método de control con nombre. La propiedad handlerName se establece como
<method name (less 'Async', if present)> . Por ejemplo, tanto "GetMessage" como "GetMessageAsync"
producen el nombre de método de control "GetMessage".
Los verbos HTTP DELETE, PUT y PATCH se convierten en POST.
Registre CustomPageApplicationModelProvider en la clase Startup :

services.AddSingleton<IPageApplicationModelProvider,
CustomPageApplicationModelProvider>();

El modelo de página en Index.cshtml.cs muestra cómo se cambian las convenciones de nomenclatura de métodos
de control habituales de las páginas de la aplicación. La nomenclatura con el prefijo "On" típicamente usado en
páginas de Razor se quita. El método que inicializa el estado de página ahora se denomina Get . Esta convención,
usada a lo largo de la aplicación, se puede ver si se abre un modelo de página de cualquiera de las páginas.
El resto de métodos comienzan por el verbo HTTP, que describe su procesamiento. Los dos métodos que
empiezan por Delete se tratarían normalmente como verbos HTTP DELETE, pero la lógica de
TryParseHandlerMethod establece explícitamente el verbo POST para ambos métodos de control.

Recuerde que Async es opcional entre DeleteAllMessages y DeleteMessageAsync . Ambos son métodos
asincrónicos, pero puede optar entre usar el postfijo Async o no. Le recomendamos que lo haga.
DeleteAllMessages se usa aquí con fines de demostración, pero se recomienda denominar este método
DeleteAllMessagesAsync . Aunque no afecta al procesamiento de la implementación de ejemplo, usar el postfijo
Async resalta el hecho de que es un método asincrónico.
public async Task Get()
{
Messages = await _db.Messages.AsNoTracking().ToListAsync();
}

public async Task<IActionResult> PostMessageAsync()


{
_db.Messages.Add(Message);
await _db.SaveChangesAsync();

Result = $"{nameof(PostMessageAsync)} handler: Message '{Message.Text}' added.";

return RedirectToPage();
}

public async Task<IActionResult> DeleteAllMessages()


{
foreach (Message message in _db.Messages)
{
_db.Messages.Remove(message);
}
await _db.SaveChangesAsync();

Result = $"{nameof(DeleteAllMessages)} handler: All messages deleted.";

return RedirectToPage();
}

public async Task<IActionResult> DeleteMessageAsync(int id)


{
var message = await _db.Messages.FindAsync(id);

if (message != null)
{
_db.Messages.Remove(message);
await _db.SaveChangesAsync();
}

Result = $"{nameof(DeleteMessageAsync)} handler: Message with Id: {id} deleted.";

return RedirectToPage();
}

Fíjese en que los nombres de control proporcionados en Index.cshtml coinciden con los métodos de control
DeleteAllMessages y DeleteMessageAsync :
<div class="row">
<div class="col-md-3">
<form method="post">
<h2>Clear all messages</h2>
<hr>
<div class="form-group">
<button type="submit" asp-page-handler="DeleteAllMessages"
class="btn btn-danger">Clear All</button>
</div>
</form>
</div>
</div>

<div class="row">
<div class="col-md-12">
<form method="post">
<h2>Messages</h2>
<hr>
<ol>
@foreach (var message in Model.Messages)
{
<li>
@message.Text
<button type="submit" asp-page-handler="DeleteMessage"
class="btn btn-danger"
asp-route-id="@message.Id">Delete</button>
</li>
}
</ol>
</form>
</div>

Async en el nombre del método de control DeleteMessageAsync se descarta por medio de TryParseHandlerMethod
para que coincida con la solicitud POST al método. El nombre asp-page-handler de DeleteMessage coincide con el
método de control DeleteMessageAsync .

Filtros de MVC y filtro de página (IPageFilter)


Las páginas de Razor no tienen en cuenta los filtros de acciones de MVC, porque en este tipo de páginas se usan
métodos de control. Hay disponibles otros tipos de filtro de MVC que puede usar: Authorization, Exception,
Resource y Result. Para más información, vea el tema Filters (Filtros).
El filtro de página (IPageFilter) es un filtro que se aplica a 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).

Recursos adicionales
Convenciones de autorización de las páginas de Razor
Cargar archivos en una página de Razor en ASP.NET
Core
27/08/2018 • 26 minutes to read • Edit Online

Por Luke Latham


En este tema se basa en el aplicación de ejemplo en Introducción a las páginas de Razor en ASP.NET Core.
En este tema se muestra cómo utilizar el enlace modelo simple para cargar archivos, 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; }
}
}

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

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.
// a display name.
MemberInfo property =
typeof(FileUpload).GetProperty(formFile.Name.Substring(
formFile.Name.IndexOf(".") + 1));

if (property != null)
{
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)
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; }
}
}
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 RazorPagesMovieContext
Especifique DbSet en RazorPagesMovieContext (Data/RazorPagesMovieContext.cs) para las programaciones:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;

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

public DbSet<RazorPagesMovie.Models.Movie> Movie { get; set; }


public DbSet<RazorPagesMovie.Models.Schedule> Schedule { get; set; }
}
}

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

@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)
@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.RazorPagesMovieContext _context;

public IndexModel(RazorPagesMovie.Models.RazorPagesMovieContext context)


{
_context = context;
}

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

public IList<Schedule> Schedule { get; private 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,
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");
}
}
}

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

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

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

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");
}
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 Pages/Shared/_Layout.cshtml y agregue un vínculo a la barra de navegación para llegar a la página de
programaciones:

<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>
@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.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.RazorPagesMovieContext _context;

public DeleteModel(RazorPagesMovie.Models.RazorPagesMovieContext 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");
}
}
}
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");
}

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.
SDK de Razor de ASP.NET Core
27/08/2018 • 7 minutes to read • Edit Online

Por Rick Anderson


El SDK de .NET Core 2.1 o versiones posteriores incluye el SDK de MSBuild Microsoft.NET.Sdk.Razor (SDK de
Razor). El SDK de Razor:
Normaliza la experiencia de creación, empaquetado y publicación de proyectos que contienen archivos de
Razor relativos a proyectos basados en ASP.NET Core MVC.
Incluye un conjunto de destinos, propiedades y elementos predefinidos que permiten personalizar la
compilación de archivos de Razor.

Requisitos previos
SDK de .NET Core 2.1 o versiones posteriores

Uso del SDK de Razor


En la mayoría de las aplicaciones web no es necesario hacer una referencia expresa al SDK de Razor.
Para usar el SDK de Razor para crear bibliotecas de clases que contengan vistas de Razor o páginas de Razor:
Use Microsoft.NET.Sdk.Razor en lugar de Microsoft.NET.Sdk :

<Project SDK="Microsoft.NET.Sdk.Razor">
...
</Project>

Las referencias de paquete a Microsoft.AspNetCore.Mvc suelen ser necesarias para incluir más
dependencias que son necesarias para generar y compilar páginas de Razor y vistas de Razor. Como
mínimo, el proyecto debe tener agregadas referencias de paquete a:
Microsoft.AspNetCore.Razor.Design
Microsoft.AspNetCore.Mvc.Razor.Extensions

Los paquetes anteriores están incluidos en Microsoft.AspNetCore.Mvc . En el siguiente marcado se muestra un


archivo .csproj básico que usa el SDK de Razor para crear archivos de Razor para una aplicación de páginas de
Razor de ASP.NET Core:

<Project Sdk="Microsoft.NET.Sdk.Razor">

<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.1.0" />
</ItemGroup>

</Project>

Propiedades
Las siguientes propiedades controlan el comportamiento del SDK de Razor como parte de la generación de un
proyecto:
RazorCompileOnBuild : si es true , compila y emite el ensamblado de Razor como parte de la generación del
proyecto. Tiene como valor predeterminado true .
RazorCompileOnPublish : si es true , compila y emite el ensamblado de Razor como parte de la publicación del
proyecto. Tiene como valor predeterminado true .
Para configurar las entradas y la salida del SDK de Razor se usan las siguientes propiedades y elementos:

ELEMENTOS DESCRIPCIÓN

RazorGenerate Elementos (archivos .cshtml) que son entradas para los


destinos de generación de código.

RazorCompile Elementos (archivos .cs) que son entradas para los destinos de
compilación de Razor. Use este ItemGroup para especificar
otros archivos que se compilen en el ensamblado de Razor.

RazorTargetAssemblyAttribute Elementos que se usan para generar atributos de código para


el ensamblado de Razor. Por ejemplo:
<RazorAssemblyAttribute
Include="System.Reflection.AssemblyMetadataAttribute"
_Parameter1="BuildSource"
_Parameter2="https://docs.asp.net/">

RazorEmbeddedResource Elementos que se agregan como recursos insertados en el


ensamblado de Razor generado.

PROPERTY DESCRIPCIÓN

RazorTargetName Nombre de archivo (sin extensión) del ensamblado generado


por Razor.

RazorOutputPath Directorio de salida de Razor.

RazorCompileToolset Sirve para determinar el conjunto de herramientas que se va a


usar para compilar el ensamblado de Razor. Los valores
válidos son Implicit y PrecompilationTool .

EnableDefaultContentItems Si es true , incluye ciertos tipos de archivo (como archivos


.cshtml) como contenido en el proyecto. Si la referencia se
hace a través de Microsoft.NET.Sdk.Web, también se incluyen
todos los archivos de wwwroot y los archivos de
configuración.

EnableDefaultRazorGenerateItems Si es true , incluye los archivos .cshtml de los elementos


Content en elementos RazorGenerate .

GenerateRazorTargetAssemblyInfo Si es true , genera un archivo .cs que contiene los atributos


especificados por RazorAssemblyAttribute y este se incluye
en la salida de compilación.

EnableDefaultRazorTargetAssemblyInfoAttributes Si es true , agrega a RazorAssemblyAttribute un conjunto


predeterminado de atributos de ensamblado.
PROPERTY DESCRIPCIÓN

CopyRazorGenerateFilesToPublishDirectory Si es true , copia los elementos de RazorGenerate (archivos


.cshtml) en el directorio de publicación. Los archivos de Razor
no suelen ser necesarios en una aplicación publicada si forman
parte de la compilación en tiempo de compilación o de
publicación. Tiene como valor predeterminado false .

CopyRefAssembliesToPublishDirectory Si es true , copia los elementos de referencia del ensamblado


en el directorio de publicación. Los ensamblados de referencia
no suelen ser necesarios en una aplicación publicada si la
compilación de Razor tiene lugar en tiempo de compilación o
de publicación. Establézcala en true si la aplicación
publicada requiere compilación en tiempo de ejecución, por
ejemplo, si modifica archivos cshtml en tiempo de ejecución o
si usa vistas incrustadas. Tiene como valor predeterminado
false .

IncludeRazorContentInPack Si es true , todos los elementos de contenido de Razor


(archivos .cshtml) se marcarán para incluirse en el paquete
NuGet generado. Tiene como valor predeterminado false .

EmbedRazorGenerateSources Si es true , agrega los elementos de RazorGenerate (.cshtml)


como archivos incrustados al ensamblado de Razor generado.
Tiene como valor predeterminado false .

UseRazorBuildServer Si es true , usa un proceso de servidor de compilación


persistente para descargar el trabajo de generación de código.
El valor predeterminado es UseSharedCompilation .

Destinos
El SDK de Razor establece dos objetivos principales:
RazorGenerate : el código genera archivos .cs a partir de elementos de RazorGenerate. Use la propiedad
RazorGenerateDependsOn para especificar más destinos que puedan ejecutarse antes o después de este destino.
RazorCompile : compila los archivos .cs generados en un ensamblado de Razor. Use RazorCompileDependsOn
para especificar más destinos que puedan ejecutarse antes o después de este destino.
Compilación en tiempo de ejecución de vistas de Razor
El SDK de Razor no publica de forma predeterminada los ensamblados de referencia necesarios para
realizar una compilación en tiempo de ejecución. Como consecuencia, se producen errores de compilación
cuando el modelo de aplicación se basa en la compilación en tiempo de ejecución; por ejemplo, la
aplicación usa vistas incrustadas o cambia vistas tras haber sido publicada. Establezca
CopyRefAssembliesToPublishDirectory en true para seguir publicando ensamblados de referencia.

En el caso de las aplicaciones web, asegúrese de que la aplicación se destina al SDK Microsoft.NET.Sdk.Web .
Información general de ASP.NET Core MVC
31/08/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] es el atributo que se usa para crear filtros de autorización MVC.
[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 incluye compatibilidad con la negociación de contenido HTTP y compatibilidad integrada para aplicar
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
Al usar interfaces e inserción de dependencias, el marco es adecuado para realizar pruebas unitarias. También
incluye características (por ejemplo, un proveedor TestHost e InMemory para Entity Framework) con las que
resulta muy fácil y rápido realizar pruebas de integración. 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.

Versión de compatibilidad
El método SetCompatibilityVersion permite a una aplicación participar o no en los cambios de
comportamiento importantes incorporados en ASP.NET Core MVC 2.1 o una versión posterior.
Para obtener más información, vea Versión de compatibilidad para ASP.NET Core MVC.
Enlace de modelos en ASP.NET Core
31/08/2018 • 16 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.

Personalizar la validación y el enlace de modelos de forma


global
El comportamiento del enlace de modelos y la validación del sistema se controla con
ModelMetadata, que describe lo siguiente:
Cómo se va a enlazar un modelo.
Cómo se produce la validación en el tipo y sus propiedades.
Los aspectos de comportamiento del sistema se pueden configurar de forma global mediante la
adición de un proveedor de detalles para MvcOptions.ModelMetadataDetailsProviders. MVC
tiene algunos proveedores de detalles integrados que permiten configurar el comportamiento,
como deshabilitar el enlace de modelos o la validación para tipos concretos.
Para deshabilitar el enlace de modelos en todos los modelos de un tipo determinado, agregue un
ExcludeBindingMetadataProvider en Startup.ConfigureServices . Por ejemplo, para deshabilitar
el enlace de modelos en todos los modelos del tipo System.Version :

services.AddMvc().AddMvcOptions(options =>
options.ModelMetadataDetailsProviders.Add(
new ExcludeBindingMetadataProvider(typeof(System.Version))));

Para deshabilitar la validación en las propiedades de un tipo determinado, agregue un


SuppressChildValidationMetadataProvider en Startup.ConfigureServices . Por ejemplo, para
deshabilitar la validación en las propiedades de tipo System.Guid :

services.AddMvc().AddMvcOptions(options =>
options.ModelMetadataDetailsProviders.Add(
new SuppressChildValidationMetadataProvider(typeof(System.Guid))));

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 Core selecciona formateadores de entradas en función del encabezado Content-Type y


el tipo del parámetro, a menos que tenga un atributo aplicado 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 se puede usar para crear servicios para la aplicación ASP.NET Core. 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
03/09/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. Si la validación es correcta,
IsValid devuelve un código ValidationResult.Success . Si se produce un error de validación, se
devuelve un ValidationResult con un mensaje de error:

private string GetErrorMessage()


{
return $"Classic movies must have a release year earlier than {_year}.";
}

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.unobtr
usive.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 de validación de cliente
classicmovie personalizado para el objeto validator de jQuery. Para una explicación del método
unobtrusive.adapters.add , consulte Unobtrusive Client Validation in ASP.NET MVC ( Validación
discreta de cliente en ASP.NET MVC ).

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

Con el código anterior, el método classicmovie realiza la validación del lado cliente en la fecha de
lanzamiento de la película. El mensaje de error aparece si el método 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
remoto de Validación de jQuery, la respuesta del servidor debe ser una cadena JSON que puede
ser:
"true" para elementos válidos.
"false" , undefined o null para elementos no válidos, usando el mensaje de error
predeterminado.
Si la respuesta del servidor es una cadena (por ejemplo,
"That name is already taken, try peter123 instead" ), la cadena se muestra como un mensaje de
error personalizado en lugar de la cadena predeterminada.
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
02/08/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.
Dada la mejora en la organización, es menos probable que se repitan secciones de la interfaz de usuario de
forma accidental.

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 débilmente tipados
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. El término establecimiento fuerte de tipos (o fuertemente tipado) significa
que cada variable y constante tienen un tipo definido explícitamente, 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 débilmente tipados (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 tipados,
también denominados imprecisos. 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
tipados para pasar pequeñas cantidades de datos de los controladores y las vistas, tanto en dirección de entrada
como de salida.

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 @modelespecifica 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.

@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
24/09/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
La herramienta de precompilación se eliminará en ASP.NET Core 3.0. Se recomienda migrar al SDK de Razor.
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:

<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.ViewCompilation"
Version="2.0.4"
PrivateAssets="All" />

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 herramienta de precompilación se eliminará en ASP.NET Core 3.0. Se recomienda migrar al SDK de Razor.
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:

<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp1.1</TargetFramework>
<MvcRazorCompileOnPublish>true</MvcRazorCompileOnPublish>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore" Version="1.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="1.1.0" />
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="1.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.ViewCompilation" Version="1.1.0-*" />
</ItemGroup>
</Project>

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
Introducción a las páginas de Razor en ASP.NET Core
Vistas de ASP.NET Core MVC
Introducción a las páginas de Razor en ASP.NET Core
Vistas de ASP.NET Core MVC
SDK de Razor de ASP.NET Core
Diseño en ASP.NET Core
31/08/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 obtendrá información sobre cómo usar diseños comunes, compartir directivas y ejecutar
código común antes de representar vistas en la aplicación ASP.NET Core.

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

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
31/08/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 de ASP.NET Core denominada AuthoringTagHelpers, 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 Pages/_ViewImports.cshtml, que
heredan de forma predeterminada todos los archivos contenidos en la carpeta Pages y sus
subcarpetas, 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 Core 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
27/08/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
30/07/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. :

@{
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
Asistentes de etiquetas integradas de ASP.NET Core
20/09/2018 • 2 minutes to read • Edit Online

Por Peter Kellner


ASP.NET Core incluye varios asistentes de etiquetas integradas para aumentar la productividad. En esta sección
se proporciona un resumen de los asistentes de etiquetas integradas.

NOTE
Algunos asistentes de etiquetas integradas no figuran en la sección, ya que se usan internamente en el motor de vistas de
Razor. Esto incluye un asistente de etiquetas para el carácter ~, que se expande hasta la ruta de acceso raíz del sitio web.

Asistentes de etiquetas integradas de ASP.NET Core


Asistente de etiquetas de delimitador
Asistente de etiquetas de caché
Asistente de etiquetas de caché distribuida
Asistente de etiquetas de entorno
Asistente de etiquetas de formulario
Asistente de etiquetas de imagen
Asistente de etiquetas de entrada
Asistente de etiquetas de elementos de etiqueta
Asistente de etiquetas parciales
Asistente de etiquetas de selección
Asistente de etiquetas de área de texto
Asistente de etiquetas de mensaje de validación
Asistente de etiquetas de resumen de validación

Recursos adicionales
Aplicaciones auxiliares de etiquetas en ASP.NET Core
Tag Helper Components in ASP.NET Core
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
31/08/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é. De manera
predeterminada, 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 Core 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
30/07/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 caché en memoria en ASP.NET Core
Introducción a la identidad en ASP.NET Core
Aplicación auxiliar de etiquetas de entorno en
ASP.NET Core
30/07/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
Aplicaciones auxiliares de etiquetas en formularios de
ASP.NET Core
30/07/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. :

@{
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 caché en memoria en ASP.NET Core
Aplicaciones auxiliares de etiquetas en formularios de
ASP.NET Core
30/07/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. :

@{
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
30/07/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. :

@{
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
02/08/2018 • 5 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. Para conocer todos los valores name aceptables, consulte
Detección de vistas parciales.
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.

Migración desde una aplicación auxiliar HTML


Tenga en cuenta el siguiente ejemplo de aplicación auxiliar HTML asincrónica. Se itera y se muestra una colección
de productos. Según el primer parámetro del método PartialAsync , se carga la vista parcial
_ProductPartial.cshtml. Se pasa una instancia del modelo Product a la vista parcial para el enlace.

@foreach (var product in Model.Products)


{
@await Html.PartialAsync("_ProductPartial", product)
}

La siguiente aplicación auxiliar de etiquetas parciales logra el mismo comportamiento de representación


asincrónica que la aplicación auxiliar HTML PartialAsync . El atributo model tiene asignada una instancia del
modelo Product para el enlace a la vista parcial.

@foreach (var product in Model.Products)


{
<partial name="_ProductPartial" model="product" />
}
Recursos adicionales
Vistas parciales en ASP.NET Core
Vistas de ASP.NET Core MVC
Aplicaciones auxiliares de etiquetas en formularios de
ASP.NET Core
30/07/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. :

@{
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
30/07/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. :

@{
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
30/07/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. :

@{
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
30/07/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. :

@{
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
14/08/2018 • 12 minutes to read • Edit Online

Por Steve Smith, Maher JENDOUBI, Rick Anderson y Scott Sauber


ASP.NET Core es compatible con las vistas parciales. Las vistas parciales se utilizan para compartir partes
reutilizables de páginas web en 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.
Por ejemplo, la plantilla de proyecto Aplicación web de ASP.NET Core 2.1 incluye una vista parcial
_CookieConsentPartial.cshtml. La vista parcial se carga desde _Layout.cshtml:

¿Cuándo se usan las 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.
En una página compleja compuesta por varias partes lógicas, puede ser útil trabajar con cada parte como su
propia vista parcial. Cada parte de la página puede verse de forma aislada del resto de la página. La vista de la
propia página se vuelve más sencilla, puesto que solo contiene la estructura general de la página y las
llamadas para representar las vistas parciales.
Los controladores ASP.NET Core MVC tienen un método PartialView que se llama desde un método de
acción. Razor Pages no tiene ningún método PartialView equivalente en PageModel.

Declaración de vistas parciales


Las vistas parciales se crean como una vista normal: se crea un archivo .cshtml dentro de la carpeta Views. No
hay ninguna diferencia semántica entre una vista parcial y una vista normal, pero se representan de forma
diferente. Puede hacer que una vista se devuelva directamente desde la clase 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. Las vistas normales sí
ejecutan _ViewStart.cshtml. Más información sobre _ViewStart.cshtml en Diseño.
Por convención, los nombres de archivo de las vistas parciales suelen empezar por _ . Esta convención de
nomenclatura no es un requisito, pero resulta útil para diferenciar visualmente las vistas parciales de las vistas
normales.

Referencia a una vista parcial


Dentro de una página de vista, hay varias maneras de representar una vista parcial. El procedimiento
recomendado es usar el procesamiento asincrónico.
Aplicación auxiliar de etiquetas parciales
La aplicación auxiliar de etiquetas parciales requiere ASP.NET Core 2.1 o posterior. Representa
asincrónicamente y usa una sintaxis similar a HTML:

<partial name="_AuthorPartial" />

Para obtener más información, vea Aplicación auxiliar de etiquetas parciales en ASP.NET Core.
Aplicación auxiliar HTML asincrónica
Cuando se usa una aplicación auxiliar HTML, el procedimiento recomendado es usar PartialAsync. Devuelve
un tipo IHtmlContent encapsulado en una clase Task . Para hacer referencia al método, se agrega a la llamada
el prefijo @ :

@await Html.PartialAsync("_AuthorPartial")

También puede representar una vista parcial con RenderPartialAsync. Este método no devuelve ningún
resultado, sino que transmite por secuencias la salida representada directamente a la respuesta. Como el
método 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 .
Aplicación auxiliar HTML sincrónica
Partial y RenderPartial son los equivalentes sincrónicos de PartialAsync y RenderPartialAsync ,
respectivamente. El uso de los equivalentes sincrónicos no es aconsejable, ya que hay escenarios donde
producen interbloqueos. Las versiones futuras no contendrán métodos sincrónicos.

IMPORTANT
Si las vistas necesitan ejecutar código, use un componente de vista en lugar de una vista parcial.

En ASP.NET Core 2.1 o posterior, una llamada a Partial o a RenderPartial da como resultado una
advertencia de analizador. Por ejemplo, el uso de Partial da como resultado el mensaje de advertencia
siguiente:

Use of IHtmlHelper.Partial may result in application deadlocks. Consider using <partial> Tag Helper or
IHtmlHelper.PartialAsync . ( El uso de IHtmlHelper.Partial puede producir interbloqueos de aplicaciones.
Considere el uso de la aplicación auxiliar de etiquetas <partial> o IHtmlHelper.PartialAsync .)

Reemplace las llamadas a @Html.Partial por @await Html.PartialAsync o la aplicación auxiliar de etiquetas
parciales. Para más información sobre la migración de la aplicación auxiliar de etiquetas parciales, vea
Migración desde una aplicación auxiliar HTML.

Detección de vistas parciales


Cuando se hace referencia a una vista parcial, se puede hacer referencia a su ubicación de varias maneras. Por
ejemplo:

// Uses a view in current folder with this name.


// If none is found, searches the Shared folder.
<partial name="_ViewName" />

// A view with this name must be in the same folder


<partial name="_ViewName.cshtml" />

// Locate the view based on the app root.


// Paths that start with "/" or "~/" refer to the app root.
<partial name="~/Views/Folder/_ViewName.cshtml" />
<partial name="/Views/Folder/_ViewName.cshtml" />

// Locate the view using a relative path


<partial name="../Account/_LoginPartial.cshtml" />

En el ejemplo anterior se usa la aplicación auxiliar de etiquetas parciales, lo que requiere ASP.NET Core 2.1 o
posterior. En el ejemplo siguiente se usan aplicaciones auxiliares HTML asincrónicas para realizar la misma
tarea.

// 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 app root.


// Paths that start with "/" or "~/" refer to the app root.
@await Html.PartialAsync("~/Views/Folder/_ViewName.cshtml")
@await Html.PartialAsync("/Views/Folder/_ViewName.cshtml")

// Locate the view using a relative path


@await Html.PartialAsync("../Account/_LoginPartial.cshtml")

Puede tener diferentes vistas parciales con el mismo nombre de archivo 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 usan
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. Las vistas que no tengan su propia versión de la vista parcial
usarán 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
Una section de Razor definida en una vista parcial no es visible para las vistas principales. La section solo es visible
para la vista parcial en la que está definida.

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. Los cambios de ViewData en una vista parcial se pierden cuando se devuelve la vista parcial.
Puede pasar una instancia de ViewDataDictionary a la vista parcial:

@await Html.PartialAsync("_PartialName", customViewData)

Puede pasar un modelo a una vista parcial. El modelo puede ser el 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:

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. Use la sobrecarga del constructor de
ViewDataDictionary resaltado para pasar un nuevo diccionario de ViewData a la vez que conserva el
diccionario ViewData existente.

@model PartialViewsSample.ViewModels.Article

<h2>@Model.Title</h2>
@* Pass the author's 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(ViewData)
{
{ "index", index }
})

index++;
}
}

Views/Shared/_AuthorPartial:

@model string
<div>
<h3>@Model</h3>
This partial view from /Views/Shared/_AuthorPartial.cshtml.
</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.

Recursos adicionales
Referencia de sintaxis de Razor para ASP.NET Core
Aplicaciones auxiliares de etiquetas en ASP.NET Core
Aplicación auxiliar de etiquetas parciales en ASP.NET Core
Componentes de vista en ASP.NET Core
Referencia de sintaxis de Razor para ASP.NET Core
Componentes de vista en ASP.NET Core
Inserción de dependencias en vistas de ASP.NET
Core
29/06/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" };
}
}
}
IMPORTANT
No olvide registrar los tipos que solicite a través de la inserción de dependencias en Startup.ConfigureServices . Un
tipo no registrado produce una excepción en tiempo de ejecución porque el proveedor de servicios se consulta
internamente a través de GetRequiredService.

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
31/08/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:
/Pages/Components//<nombre_de_vista>
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
06/08/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 IActionResult Edit(int id, Customer customer)


{
if (ModelState.IsValid)
{
// Update DB with new details.
return RedirectToAction("Index");
}
return View(customer);
}

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
16/07/2018 • 14 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 usa GetTempFileName , que produce una excepción IOException si se crean más
de 65 535 archivos sin eliminar los archivos temporales anteriores. Una aplicación real debe eliminar los archivos
temporales o usar GetTempPath y GetRandomFileName para crear nombres de archivo temporales. El límite de 65
535 archivos es por servidor, por lo que otra aplicación en el servidor puede usar los 65 535 archivos.

[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 usa GetTempFileName , que produce una excepción IOException si se crean más
de 65 535 archivos sin eliminar los archivos temporales anteriores. Una aplicación real debe eliminar los archivos
temporales o usar GetTempPath y GetRandomFileName para crear nombres de archivo temporales. El límite de 65
535 archivos es por servidor, por lo que otra aplicación en el servidor puede usar los 65 535 archivos.

// 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),
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);
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
06/08/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, puede corregir este problema con el uso de un solo constructor.También se
puede reemplazar el contenedor de inserción de dependencias predeterminado por una implementación de
terceros 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
31/08/2018 • 15 minutes to read • Edit Online

Por Steve Smith


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
Vea o descargue el código de ejemplo (cómo descargarlo)

Pruebas unitarias de la lógica del controlador


Las pruebas unitarias implican 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 va a escribir filtros personalizados y rutas, debería realizar pruebas unitarias en ellos de forma aislada, no como
parte de las pruebas de una acción de controlador concreta.

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
{
#region snippet_HomeController
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));


}
}
#endregion
}

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
{
#region snippet_Index_ReturnsAViewResult_WithAListOfBrainstormSessions
[Fact]
public async Task Index_ReturnsAViewResult_WithAListOfBrainstormSessions()
{
// Arrange
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.ListAsync())
.ReturnsAsync(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);
var redirectToActionResult = Assert.IsType<RedirectToActionResult>(result);
Assert.Null(redirectToActionResult.ControllerName);
Assert.Equal("Index", redirectToActionResult.ActionName);
mockRepo.Verify();
}
#endregion

#region snippet_GetTestSessions
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),

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.

}
#endregion

#region snippet_ModelState_ValidOrInvalid
[Fact]
public async Task IndexPost_ReturnsBadRequestResult_WhenModelStateIsInvalid()
{
// Arrange
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.ListAsync())
.ReturnsAsync(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

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
{
#region snippet_SessionController
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);
}
}
#endregion
}

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
{
#region snippet_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))
.ReturnsAsync((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))
.ReturnsAsync(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);
}
#endregion

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.Collections.Generic;
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 : ControllerBase
{
private readonly IBrainstormSessionRepository _sessionRepository;

public IdeasController(IBrainstormSessionRepository sessionRepository)


{
_sessionRepository = sessionRepository;
}

#region snippet_ForSessionAndCreate
[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)
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);
}
#endregion

#region snippet_ForSessionActionResult
[HttpGet("forsessionactionresult/{sessionId}")]
[ProducesResponseType(200)]
[ProducesResponseType(404)]
public async Task<ActionResult<List<IdeaDTO>>> ForSessionActionResult(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 result;
}
#endregion

#region snippet_CreateActionResult
[HttpPost("createactionresult")]
[ProducesResponseType(201)]
[ProducesResponseType(400)]
[ProducesResponseType(404)]
public async Task<ActionResult<BrainstormSession>> CreateActionResult([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()


var idea = new Idea()
{
DateCreated = DateTimeOffset.Now,
Description = model.Description,
Name = model.Name
};
session.AddIdea(idea);

await _sessionRepository.UpdateAsync(session);

return CreatedAtAction(nameof(CreateActionResult), new { id = session.Id }, session);


}
#endregion
}
}

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 de
forma manual (con un método Select de LINQ como se muestra aquí) o mediante 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
{
#region snippet_ApiIdeasControllerTests1
[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);
}
#endregion

#region snippet_ApiIdeasControllerTests2
[Fact]
public async Task Create_ReturnsHttpNotFound_ForInvalidSession()
{
// Arrange
int testSessionId = 123;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync((BrainstormSession)null);
var controller = new IdeasController(mockRepo.Object);

// Act
var result = await controller.Create(new NewIdeaModel());

// Assert
Assert.IsType<NotFoundObjectResult>(result);
}
#endregion

#region snippet_ApiIdeasControllerTests3
[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))
.ReturnsAsync(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();
// Act
var result = await controller.ForSession(testSessionId);

// Assert
var okResult = Assert.IsType<OkObjectResult>(result);
var returnValue = Assert.IsType<List<IdeaDTO>>(okResult.Value);
var idea = returnValue.FirstOrDefault();
Assert.Equal("One", idea.Name);
}
#endregion

#region snippet_ForSessionActionResult_ReturnsNotFoundObjectResultForNonexistentSession
[Fact]
public async Task ForSessionActionResult_ReturnsNotFoundObjectResultForNonexistentSession()
{

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.

Recursos adicionales
Pruebas de integración en ASP.NET Core
Temas avanzados de ASP.NET Core MVC
11/07/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
27/08/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 IFilterMetadata. Por tanto, una instancia de IFilterFactory se puede usar como
una instancia de IFilterMetadata 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 de IFilterMetadata 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 IFilterMetadata . 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
07/09/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.
Las áreas permiten organizar proyectos de MVC de gran tamaño en varios componentes generales en
los que se puede trabajar de forma independiente.
Las áreas 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
24/09/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 asistentes 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, asistentes
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 los 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)
{
apm.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
Asistentes 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.
Versión de compatibilidad para ASP.NET Core MVC
31/08/2018 • 3 minutes to read • Edit Online

Por Rick Anderson


El método SetCompatibilityVersion permite a una aplicación participar o no en los cambios de
comportamiento importantes incorporados en ASP.NET Core MVC 2.1 o una versión posterior. 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:

public void ConfigureServices(IServiceCollection services)


{
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}

Le recomendamos que pruebe la aplicación con la versión más reciente ( CompatibilityVersion.Version_2_1 ).


Prevemos que la mayoría de las aplicaciones no tendrán cambios de comportamiento importantes usando 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
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc()
// Include the 2.1 behaviors
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
// Except for the following.
.AddMvcOptions(options =>
{
// Don't combine authorize filters (keep 2.0 behavior).
options.AllowCombiningAuthorizeFilters = false;
// All exceptions thrown by an IInputFormatter are treated
// as model state errors (keep 2.0 behavior).
options.InputFormatterExceptionPolicy =
InputFormatterExceptionPolicy.AllExceptions;
});
}

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.
Compilación de API web con ASP.NET Core
24/08/2018 • 8 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:
[Produces("application/json")]
[Route("api/[controller]")]
public class PetsController : ControllerBase
{
private readonly PetsRepository _repository;

public PetsController(PetsRepository repository)


{
_repository = repository;
}

[HttpGet]
public async Task<ActionResult<List<Pet>>> GetAllAsync()
{
return await _repository.GetPetsAsync();
}

[HttpGet("{id}")]
[ProducesResponseType(404)]
public async Task<ActionResult<Pet>> GetByIdAsync(int id)
{
var pet = await _repository.GetPetAsync(id);

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

return pet;
}

[HttpPost]
[ProducesResponseType(400)]
public async Task<ActionResult<Pet>> CreateAsync(Pet pet)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}

await _repository.AddPetAsync(pet);

return CreatedAtAction(nameof(GetByIdAsync),
new { id = pet.Id }, pet);
}
}
[Produces("application/json")]
[Route("api/[controller]")]
public class PetsController : ControllerBase
{
private readonly PetsRepository _repository;

public PetsController(PetsRepository repository)


{
_repository = repository;
}

[HttpGet]
[ProducesResponseType(typeof(IEnumerable<Pet>), 200)]
public async Task<IActionResult> GetAllAsync()
{
var pets = await _repository.GetPetsAsync();

return Ok(pets);
}

[HttpGet("{id}")]
[ProducesResponseType(typeof(Pet), 200)]
[ProducesResponseType(404)]
public async Task<IActionResult> GetByIdAsync(int id)
{
var pet = await _repository.GetPetAsync(id);

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

return Ok(pet);
}

[HttpPost]
[ProducesResponseType(typeof(Pet), 201)]
[ProducesResponseType(400)]
public async Task<IActionResult> CreateAsync([FromBody] Pet pet)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}

await _repository.AddPetAsync(pet);

return CreatedAtAction(nameof(GetByIdAsync),
new { id = pet.Id }, pet);
}
}

La clase ControllerBase proporciona acceso a varios métodos y propiedades. En el código anterior, algunos
ejemplos son BadRequest(ModelStateDictionary) y CreatedAtAction(String, Object, Object). Se llama a estos
métodos 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 controlar 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:
[Route("api/[controller]")]
[ApiController]
public class ProductsController : ControllerBase

Para usar este atributo se requiere una versión de compatibilidad de 2.1 o posterior, establecida mediante
SetCompatibilityVersion. Por ejemplo, el código resaltado en Startup.ConfigureServices establece la marca de
compatibilidad 2.1:

services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

Para obtener más información, vea Versión de compatibilidad para ASP.NET Core MVC.
El atributo [ApiController] se suele emparejar con ControllerBase para habilitar el comportamiento específico
de REST para los controladores. 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] :

[ApiController]
public class MyBaseController
{
}

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:

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

El comportamiento predeterminado se deshabilita al establecer la propiedad SuppressModelStateInvalidFilter


en true . Agregue el siguiente código en Startup.ConfigureServices después de
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); :

services.Configure<ApiBehaviorOptions>(options =>
{
options.SuppressConsumesConstraintForFormFileParameters = true;
options.SuppressInferBindingSourcesForParameters = true;
options.SuppressModelStateInvalidFilter = true;
});

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


ATRIBUTO ORIGEN DE ENLACE

[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

WARNING
No use [FromRoute] si los valores pueden contener %2f (es decir, / ). %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:

[HttpGet]
public async Task<ActionResult<List<Product>>> GetAsync(
[FromQuery] bool discontinuedOnly = false)
{
List<Product> products = null;

if (discontinuedOnly)
{
products = await _repository.GetDiscontinuedProductsAsync();
}
else
{
products = await _repository.GetProductsAsync();
}

return products;
}

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 y CancellationToken. El código
de inferencia del origen de enlace omite esos tipos especiales. En el caso de los tipos simples, como string y
int , [FromBody] no se infiere. Así pues, para los tipos simples, en los casos en los que quiera utilizar dicha
funcionalidad, conviene usar el atributo [FromBody] . 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:
// Don't do this. All of the following actions result in an exception.
[HttpPost]
public IActionResult Action1(Product product,
Order order) => null;

[HttpPost]
public IActionResult Action2(Product product,
[FromBody] Order order) => null;

[HttpPost]
public IActionResult Action3([FromBody] Product product,
[FromBody] Order order) => null;

[FromForm ] se infiere para los parámetros de acción de tipo IFormFile y 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 al establecer la propiedad


SuppressInferBindingSourcesForParameters en true . Agregue el siguiente código en
Startup.ConfigureServices después de
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); :

services.Configure<ApiBehaviorOptions>(options =>
{
options.SuppressConsumesConstraintForFormFileParameters = true;
options.SuppressInferBindingSourcesForParameters = true;
options.SuppressModelStateInvalidFilter = true;
});

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 al establecer la propiedad


SuppressConsumesConstraintForFormFileParameters en true . Agregue el siguiente código en
Startup.ConfigureServices después de
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); :

services.Configure<ApiBehaviorOptions>(options =>
{
options.SuppressConsumesConstraintForFormFileParameters = true;
options.SuppressInferBindingSourcesForParameters = true;
options.SuppressModelStateInvalidFilter = true;
});

Requisito de enrutamiento mediante atributos


El enrutamiento mediante atributos pasa a ser un requisito. Por ejemplo:

[Route("api/[controller]")]
[ApiController]
public class ProductsController : ControllerBase
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 de ASP.NET Core Web API
Formateadores personalizados en ASP.NET Core Web API
Aplicación de formato a datos de respuesta en ASP.NET Core Web API
Páginas de ayuda de ASP.NET Core Web API con Swagger/Open API
Enrutar a acciones de controlador de ASP.NET Core
Tipos de valor devuelto de acción del controlador
de ASP.NET Core Web API
10/09/2018 • 9 minutes to read • Edit Online

Por Scott Addie


Vea o descargue el código de ejemplo (cómo descargarlo)
ASP.NET Core ofrece las siguientes opciones relativas a los tipos de valor devuelto de acción del controlador de
Web API:
Tipo específico
IActionResult
ActionResult<T>
Tipo específico
IActionResult
En este documento se explica cuándo resulta más adecuado usar cada tipo de valor devuelto.

Tipo específico
La acción más sencilla devuelve un tipo de datos primitivo o complejo (por ejemplo, string o un tipo de objeto
personalizado). Consideremos la siguiente acción, que devuelve una colección de objetos Product
personalizados:

[HttpGet]
public IEnumerable<Product> Get()
{
return _repository.GetProducts();
}

Sin condiciones conocidas de las que haya que protegerse durante la ejecución de la acción, bastaría con que se
devolviera un tipo específico. La acción anterior no acepta parámetros, por lo que no se necesita ninguna
validación de restricciones de parámetros.
Si existen condiciones conocidas que deban tenerse en cuenta en una acción, se presentan varias rutas de acceso
de valor devuelto. En tal caso, lo habitual es mezclar un tipo devuelto ActionResult con el tipo de valor devuelto
primitivo o complejo. Se necesitará IActionResult o ActionResult<T> para dar cabida a este tipo de acción.

Tipo IActionResult
El tipo de valor devuelto IActionResult resulta adecuado cuando existen varios tipos de valor devuelto
ActionResult posibles en una acción. Los tipos ActionResult representan varios códigos de estado HTTP.
Algunos de los tipos de valor devuelto que suelen entrar en esta categoría son BadRequestResult (400),
NotFoundResult (404) y OkObjectResult (200).
Dado que hay varios tipos de valor devuelto y rutas de acceso en la acción, es necesario el uso libre del atributo
[ProducesResponseType]. Este atributo genera detalles de respuesta más pormenorizados relativos a las páginas
de ayuda de API generadas por herramientas como Swagger. [ProducesResponseType] indica los tipos conocidos
y los códigos de estado HTTP que la acción va a devolver.
Acción sincrónica
Veamos la siguiente acción sincrónica, en la que pueden darse dos tipos de valor devuelto posibles:

[HttpGet("{id}")]
[ProducesResponseType(200, Type = typeof(Product))]
[ProducesResponseType(404)]
public IActionResult GetById(int id)
{
if (!_repository.TryGetProduct(id, out var product))
{
return NotFound();
}

return Ok(product);
}

En la acción anterior, se devuelve un código de estado 404 cuando el producto representado por id no existe en
el almacén de datos subyacente. El método auxiliar NotFound se invoca como una forma abreviada de
return new NotFoundResult(); . Si el producto existe, se devuelve un objeto Product que representa la carga con
un código de estado 200. El método auxiliar Ok se invoca como una forma abreviada de
return new OkObjectResult(product); .

Acción asincrónica
Veamos la siguiente acción asincrónica, en la que pueden darse dos tipos de valor devuelto posibles:

[HttpPost]
[ProducesResponseType(201, Type = typeof(Product))]
[ProducesResponseType(400)]
public async Task<IActionResult> CreateAsync([FromBody] Product product)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}

await _repository.AddProductAsync(product);

return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);


}

En la acción anterior, se devuelve un código de estado 400 cuando se produce un error en la validación de
modelos y se invoca el método auxiliar BadRequest. Por ejemplo, el siguiente modelo indica que las solicitudes
deben proporcionar la propiedad Name y un valor. Por lo tanto, si no se proporciona una propiedad Name
adecuada en la solicitud, se producirá un error en la validación de modelos.

public class Product


{
public int Id { get; set; }

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

public string Description { get; set; }


}

Otro código de retorno conocido de la acción anterior es un 201, que se genera con el método auxiliar
CreatedAtAction. En esta ruta de acceso, se devuelve el objeto Product .
Tipo ActionResult<T>
ASP.NET Core 2.1 incorpora el tipo de valor devuelto ActionResult<T> para las acciones de controlador de Web
API. Permite devolver un tipo que se deriva de ActionResult o bien un tipo específico. ActionResult<T> reporta
las siguientes ventajas con frente al tipo IActionResult:
La propiedad Type del atributo [ProducesResponseType] se puede excluir. Por ejemplo,
[ProducesResponseType(200, Type = typeof(Product))] se simplifica a [ProducesResponseType(200)] . En su
lugar, el tipo de valor devuelto esperado de la acción se infiere desde T en ActionResult<T> .
Los operadores de conversión implícitos admiten la conversión tanto de T como de ActionResult en
ActionResult<T> . T se convierte en ObjectResult, lo que significa que return new ObjectResult(T); se ha
simplificado para return T; .
C# no admite operadores de conversión implícitos en las interfaces. Por consiguiente, la conversión de la interfaz
a un tipo concreto es necesaria para usar ActionResult<T> . Por ejemplo, el uso de IEnumerable en el siguiente
ejemplo no funciona:

[HttpGet]
public ActionResult<IEnumerable<Product>> Get()
{
return _repository.GetProducts();
}

Una opción para corregir el código anterior es devolver _repository.GetProducts().ToList(); .


La mayoría de las acciones tiene un tipo de valor devuelto específico. Se pueden producir condiciones
inesperadas que durante la ejecución de una acción, en cuyo caso no se devuelve el tipo específico. Por ejemplo,
puede suceder que el parámetro de entrada de una acción genere un error de validación de modelos. En tal caso,
lo normal es que se devuelva el tipo ActionResult correspondiente en lugar del tipo específico.
Acción sincrónica
Veamos una acción sincrónica, en la que pueden darse dos tipos de valor devuelto posibles:

[HttpGet("{id}")]
[ProducesResponseType(200)]
[ProducesResponseType(404)]
public ActionResult<Product> GetById(int id)
{
if (!_repository.TryGetProduct(id, out var product))
{
return NotFound();
}

return product;
}

En el código anterior, se devuelve un código de estado 404 cuando el producto no existe en la base de datos. Si el
producto existe, se devuelve el objeto Product correspondiente. Antes de ASP.NET Core 2.1, la línea
return product; habría sido return Ok(product); .
TIP
A partir de ASP.NET Core 2.1, la inferencia del origen de enlace de los parámetros de la acción está habilitada cuando una
clase de controlador incluye el atributo [ApiController] . Así, un nombre de parámetro que coincida con un nombre de
la plantilla de ruta se enlazará automáticamente usando los datos de ruta de la solicitud. Por lo tanto, el parámetro id de
la acción anterior no está anotado explícitamente con el atributo [FromRoute].

Acción asincrónica
Veamos una acción asincrónica, en la que pueden darse dos tipos de valor devuelto posibles:

[HttpPost]
[ProducesResponseType(201)]
[ProducesResponseType(400)]
public async Task<ActionResult<Product>> CreateAsync(Product product)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}

await _repository.AddProductAsync(product);

return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);


}

Si se produce un error en la validación de modelos, se invoca el método BadRequest para devolver un código de
estado 400. La propiedad ModelState que contiene los errores de validación específicos se pasan a dicho
método. Si la validación de modelos se realiza correctamente, el producto se crea en la base de datos. Se
devuelve un código de estado 201.

TIP
A partir de ASP.NET Core 2.1, la inferencia del origen de enlace de los parámetros de la acción está habilitada cuando una
clase de controlador incluye el atributo [ApiController] . Los parámetros de tipo complejo se enlazan automáticamente
usando el cuerpo de solicitud. Por lo tanto, el parámetro product de la acción anterior no está anotado explícitamente
con el atributo [FromBody].

Recursos adicionales
Control de solicitudes con controladores en ASP.NET Core MVC
Validación de modelos en ASP.NET Core MVC
Páginas de ayuda de ASP.NET Core Web API con Swagger/Open API
Temas avanzados sobre ASP.NET Core Web API
11/07/2018 • 2 minutes to read • Edit Online

Formateadores personalizados
Aplicación de formato a datos de respuesta
Formateadores personalizados en ASP.NET Core
Web API
29/06/2018 • 8 minutes to read • Edit Online

Por Tom Dykstra


ASP.NET Core MVC ofrece compatibilidad integrada con el intercambio de datos en las API web mediante el uso
de formatos de texto sin formato, JSON o XML. En este artículo se muestra cómo agregar compatibilidad con
formatos adicionales mediante la creación de formateadores personalizados.
Vea o descargue el código de ejemplo (cómo descargarlo)

Cuándo usar formateadores personalizados


Utilice un formateador personalizado cuando quiera que el proceso de negociación de contenido admita un tipo
de contenido que no es compatible con los formateadores integrados (JSON, XML y texto sin formato).
Por ejemplo, si algunos de los clientes de la API web pueden controlar el formato Protobuf, quizá quiera usar
Protobuf con esos clientes porque le resulte más eficaz. O tal vez desee que la API web envíe nombres y
direcciones de contacto en formato vCard, un formato comúnmente utilizado para intercambiar datos de
contacto. La aplicación de ejemplo proporcionada con este artículo implementa a un formateador de vCard
simple.

Información general sobre cómo utilizar a un formateador


personalizado
Estos son los pasos para crear y usar a un formateador personalizado:
Cree una clase de formateador de salida si quiere serializar los datos que se van a enviar al cliente.
Cree una clase de formateador de entrada si quiere deserializar los datos recibidos del cliente.
Agregue instancias de los formateadores a las colecciones InputFormatters y OutputFormatters en
MvcOptions.
Las secciones siguientes proporcionan instrucciones y ejemplos de código para cada uno de estos pasos.

Cómo crear una clase de formateador personalizado


Para crear un formateador:
Derive la clase de la clase base adecuada.
Especifique tipos de medios válidos y codificaciones en el constructor.
Invalidar métodos CanReadType / CanWriteType
Invalidar métodos ReadRequestBodyAsync / WriteResponseBodyAsync
Derivar de la clase base adecuada
Para los tipos de medios de texto (por ejemplo, vCard), se deriva de la clase base TextInputFormatter o
TextOutputFormatter.

public class VcardOutputFormatter : TextOutputFormatter


Para los tipos binarios, se deriva de la clase base InputFormatter o OutputFormatter.
Especificar las codificaciones y los tipos de medios válidos
En el constructor, especifique tipos de medios y codificaciones válidos. Para ello, agréguelos a las colecciones
SupportedMediaTypes y SupportedEncodings .

public VcardOutputFormatter()
{
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/vcard"));

SupportedEncodings.Add(Encoding.UTF8);
SupportedEncodings.Add(Encoding.Unicode);
}

NOTE
No puede realizar la inserción de dependencias de constructor en una clase de formateador. Por ejemplo, no se puede
obtener un registrador mediante la adición de un parámetro de registrador al constructor. Para obtener acceso a los
servicios, tendrá que usar el objeto de contexto que se pasa a sus métodos. A continuación se incluye un código de ejemplo
que muestra cómo hacerlo.

Invalidar CanReadType/CanWriteType
Invalide los métodos CanReadType o CanWriteType para especificar el tipo en el que se puede deserializar o desde
el que se puede serializar. Por ejemplo, quizá solo pueda crear texto de vCard desde un tipo Contact y viceversa.

protected override bool CanWriteType(Type type)


{
if (typeof(Contact).IsAssignableFrom(type)
|| typeof(IEnumerable<Contact>).IsAssignableFrom(type))
{
return base.CanWriteType(type);
}
return false;
}

Método CanWriteResult
En algunos casos tendrá que invalidar CanWriteResult en lugar de CanWriteType . Use CanWriteResult si las
condiciones siguientes son verdaderas:
El método de acción devuelve una clase de modelo.
Hay clases derivadas que pueden obtenerse en tiempo de ejecución.
Debe conocer en tiempo de ejecución qué clase derivada fue devuelta por la acción.
Por ejemplo, suponga que la firma del método de acción devuelve un tipo Person , pero puede devolver un tipo
Student o Instructor que deriva de Person . Si quiere que el formateador únicamente controle objetos
Student , compruebe el tipo del objeto en el objeto de contexto proporcionado para el método CanWriteResult .
Tenga en cuenta que no es necesario utilizar CanWriteResult cuando se devuelve el método de acción
IActionResult ; en ese caso, el método CanWriteType recibe el tipo en tiempo de ejecución.

Invalidar ReadRequestBodyAsync/WriteResponseBodyAsync
Usted se encarga de hacer el trabajo real de deserializar o serializar en ReadRequestBodyAsync o
WriteResponseBodyAsync . Las líneas resaltadas en el ejemplo siguiente muestran cómo obtener los servicios desde
el contenedor de inserción de dependencias (no es posible obtenerlos desde a partir de los parámetros del
constructor).
public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
{
IServiceProvider serviceProvider = context.HttpContext.RequestServices;
var logger = serviceProvider.GetService(typeof(ILogger<VcardOutputFormatter>)) as ILogger;

var response = context.HttpContext.Response;

var buffer = new StringBuilder();


if (context.Object is IEnumerable<Contact>)
{
foreach (Contact contact in context.Object as IEnumerable<Contact>)
{
FormatVcard(buffer, contact, logger);
}
}
else
{
var contact = context.Object as Contact;
FormatVcard(buffer, contact, logger);
}
return response.WriteAsync(buffer.ToString());
}

private static void FormatVcard(StringBuilder buffer, Contact contact, ILogger logger)


{
buffer.AppendLine("BEGIN:VCARD");
buffer.AppendLine("VERSION:2.1");
buffer.AppendFormat($"N:{contact.LastName};{contact.FirstName}\r\n");
buffer.AppendFormat($"FN:{contact.FirstName} {contact.LastName}\r\n");
buffer.AppendFormat($"UID:{contact.ID}\r\n");
buffer.AppendLine("END:VCARD");
logger.LogInformation($"Writing {contact.FirstName} {contact.LastName}");
}

Cómo configurar MVC para usar a un formateador personalizado


Para utilizar un formateador personalizado, debe agregar una instancia de la clase de formateador a la colección
InputFormatters o OutputFormatters .

services.AddMvc(options =>
{
options.InputFormatters.Insert(0, new VcardInputFormatter());
options.OutputFormatters.Insert(0, new VcardOutputFormatter());
});

Los formateadores se evalúan en el orden en que se insertaron. El primero de ellos tiene prioridad.

Pasos siguientes
Consulte la aplicación de ejemplo, en la que se implementan formateadores de entrada y salida de vCard. La
aplicación lee y escribe tarjetas vCard que tienen un aspecto similar al siguiente:

BEGIN:VCARD
VERSION:2.1
N:Davolio;Nancy
FN:Nancy Davolio
UID:20293482-9240-4d68-b475-325df4a83728
END:VCARD

Para ver la salida de vCard, ejecute la aplicación y envíe una solicitud Get con encabezado Accept "texto/vcard" a
http://localhost:63313/api/contacts/ (cuando se ejecuta desde Visual Studio) o
http://localhost:5000/api/contacts/ (cuando se ejecuta desde la línea de comandos).
Para agregar una tarjeta vCard a la colección en memoria de contactos, envíe una solicitud Post a la misma
dirección URL, con el encabezado contenido-tipo "texto/vcard" y con texto de vCard en el cuerpo. El formato sería
similar al ejemplo anterior.
Aplicación de formato a datos de respuesta en
ASP.NET Core Web API
29/06/2018 • 17 minutes to read • Edit Online

Por Steve Smith


ASP.NET Core MVC tiene compatibilidad integrada para dar formato a datos de respuesta, en función de
formatos fijos o especificaciones del cliente.
Vea o descargue el código de ejemplo (cómo descargarlo)

Resultados de acción específica de formato


Algunos tipos de resultado de acción son específicos de un formato determinado, como JsonResult y
ContentResult . Las acciones pueden devolver resultados específicos a los que siempre se da formato de una
manera determinada. Por ejemplo, si se devuelve un JsonResult , se devolverán datos con formato JSON,
independientemente de las preferencias del cliente. Del mismo modo, si se devuelve un ContentResult , se
devolverán datos de cadena con formato de texto sin formato (al igual que si se devuelve simplemente una
cadena).

NOTE
No se requieren acciones para devolver un tipo en particular, ya que MVC es compatible con cualquier valor devuelto por
un objeto. Si una acción devuelve una implementación IActionResult y el controlador hereda de Controller , los
desarrolladores disponen de diversos métodos auxiliares que se corresponden con muchas de las opciones. Los resultados
de acciones que devuelven objetos que no son tipos IActionResult se serializarán con la implementación
IOutputFormatter correspondiente.

Para devolver datos en un formato específico desde un controlador que hereda de la clase base Controller , use
el método auxiliar integrado Json para devolver JSON y Content para texto sin formato. El método de acción
debe devolver el tipo de resultado específico (por ejemplo, JsonResult ) o bien IActionResult .
Para devolver datos con formato JSON:

// GET: api/authors
[HttpGet]
public JsonResult Get()
{
return Json(_authorRepository.List());
}

Respuesta de ejemplo de esta acción:


Tenga en cuenta que el tipo de contenido de la respuesta es application/json , y se muestra en la lista de
solicitudes de red y en la sección Encabezados de respuesta. Observe también la lista de opciones que aparecen
en el explorador (en este caso, Microsoft Edge) en el encabezado Accept de la sección Encabezados de solicitud.
La técnica actual omite este encabezado; más adelante se describe su uso.
Para devolver datos con formato de texto sin formato, use ContentResult y el método auxiliar Content :

// GET api/authors/about
[HttpGet("About")]
public ContentResult About()
{
return Content("An API listing authors of docs.asp.net.");
}

Una respuesta de esta acción:


Observe que en este caso el Content-Type devuelto es text/plain . También puede obtener este mismo
comportamiento mediante el uso de un tipo de respuesta de cadena:

// GET api/authors/version
[HttpGet("version")]
public string Version()
{
return "Version 1.0.0";
}

TIP
Para las acciones no triviales con varias opciones o tipos de valor devueltos (por ejemplo, códigos de estado HTTP
diferentes en función del resultado de las operaciones realizadas), se recomienda el uso de IActionResult como tipo de
valor devuelto.

Negociación de contenido
La negociación de contenido (o conneg) se produce cuando el cliente especifica un encabezado Accept. El
formato predeterminado que usa ASP.NET Core MVC es JSON. La negociación de contenido se implementa
mediante ObjectResult . También se integra en los resultados de acción específicos del código de estado
devueltos por los métodos auxiliares (que se basan en ObjectResult ). También puede devolver un tipo de
modelo (es decir, una clase que haya definido como tipo de transferencia de datos) y el marco de trabajo lo
ajustará automáticamente en un ObjectResult .
El siguiente método de acción usa los métodos auxiliares Ok y NotFound :
// GET: api/authors/search?namelike=th
[HttpGet("Search")]
public IActionResult Search(string namelike)
{
var result = _authorRepository.GetByNameSubstring(namelike);
if (!result.Any())
{
return NotFound(namelike);
}
return Ok(result);
}

Se devolverá una respuesta con formato JSON, a menos que se haya solicitado otro formato y el servidor pueda
devolverlo. Puede usar una herramienta como Fiddler para crear una solicitud que incluya un encabezado
Accept y especifique otro formato. En ese caso, si el servidor tiene un formateador que puede producir una
respuesta en el formato solicitado, el resultado se devolverá en el formato preferido por el cliente.

En la captura de pantalla anterior, se ha usado el compositor de Fiddler para generar una solicitud mediante la
especificación de Accept: application/xml . ASP.NET Core MVC solo admite JSON de forma predeterminada,
por lo que, incluso si se especifica otro formato, el resultado devuelto tendrá formato JSON. En la sección
siguiente verá cómo agregar otros formateadores.
Las acciones del controlador pueden devolver POCO (objetos CLR estándar), en cuyo caso ASP.NET Core MVC
crea automáticamente un ObjectResult que ajuste el objeto. El cliente obtendrá el objeto serializado con
formato (el formato predeterminado es JSON, pero puede configurar XML u otros formatos). Si el objeto que se
devuelve es null , el marco de trabajo devolverá una respuesta 204 No Content .
Devolución de un tipo de objeto:

// GET api/authors/ardalis
[HttpGet("{alias}")]
public Author Get(string alias)
{
return _authorRepository.GetByAlias(alias);
}

En el ejemplo, una solicitud de un alias de autor válido recibirá una respuesta "200 - Correcto" con los datos del
autor. Una solicitud de un alias no válido recibirá una respuesta "204 Sin contenido". A continuación se muestran
capturas de pantalla en las que aparece la respuesta en formatos XML y JSON.
Proceso de negociación de contenido
La negociación de contenido solo se lleva a cabo si en la solicitud aparece un encabezado Accept . Si una
solicitud contiene un encabezado Accept, el marco de trabajo enumerará los tipos de medios en el encabezado
Accept en orden de preferencia e intentará encontrar un formateador que pueda generar una respuesta en uno
de los formatos especificados por dicho encabezado. En caso de que no se encuentre ningún formateador que
satisfaga la solicitud del cliente, el marco de trabajo intentará encontrar el primer formateador que pueda
generar una respuesta (a menos que el desarrollador haya configurado la opción en MvcOptions para devolver
"406 - No aceptable"). Si la solicitud especifica XML pero no se ha configurado el formateador XML, se usará el
formateador JSON. Por lo general, si no se ha configurado ningún formateador que proporcione el formato
solicitado, se usará el primer formateador que pueda dar formato al objeto. Si no se especifica ningún
encabezado, se usará para serializar la respuesta el primer formateador que pueda controlar el objeto que se va
a devolver. En este caso no se produce ninguna negociación, ya que es el servidor el que determina qué formato
usará.

NOTE
Si el encabezado Accept contiene */* , el encabezado se omitirá a menos que RespectBrowserAcceptHeader esté
establecido en true en MvcOptions .

Exploradores y negociación de contenido


A diferencia de los clientes de API típicos, los exploradores web suelen proporcionar encabezados Accept que
incluyen una amplia gama de formatos, incluidos caracteres comodín. De forma predeterminada, cuando el
marco de trabajo detecta que la solicitud procede de un explorador, omite el encabezado Accept y devuelve el
contenido en el formato predeterminado configurado de la aplicación (JSON, a menos que se haya configurado
otro). Esto proporciona una experiencia más coherente al usar exploradores diferentes para el consumo de API.
Si prefiere que la aplicación respete los encabezados Accept del explorador, puede incluirlo en la configuración
de MVC mediante el establecimiento de RespectBrowserAcceptHeader en true en el método ConfigureServices
de Startup.cs.

services.AddMvc(options =>
{
options.RespectBrowserAcceptHeader = true; // false by default
});

Configuración de formateadores
Si la aplicación necesita admitir formatos adicionales además del formato JSON predeterminado, puede agregar
paquetes NuGet y configurar MVC para que los admita. Hay formateadores independientes para la entrada y la
salida. El enlace de modelos usa formateadores de entrada, mientras que los formateadores de salida se usan
para dar formato a las respuestas. También puede configurar formateadores personalizados.
Agregar compatibilidad con el formato XML
Para agregar compatibilidad con el formato XML, instale el paquete NuGet
Microsoft.AspNetCore.Mvc.Formatters.Xml .

Agregue los formateadores serializadores de XML a la configuración de MVC en Startup.cs:

services.AddMvc()
.AddXmlSerializerFormatters();

También puede agregar simplemente el formateador de salida:

services.AddMvc(options =>
{
options.OutputFormatters.Add(new XmlSerializerOutputFormatter());
});

Estos dos métodos serializarán los resultados con System.Xml.Serialization.XmlSerializer . Si lo prefiere, puede
usar System.Runtime.Serialization.DataContractSerializer mediante la adición de su formateador asociado:
services.AddMvc(options =>
{
options.OutputFormatters.Add(new XmlDataContractSerializerOutputFormatter());
});

Una vez que se ha agregado compatibilidad con el formato XML, los métodos de controlador deben devolver el
formato adecuado en función del encabezado Accept de la solicitud, como se muestra en este ejemplo de
Fiddler:

En la pestaña Inspectores puede ver que la solicitud GET sin formato se ha realizado con un conjunto de
encabezados Accept: application/xml . En el panel de respuesta se muestra el encabezado
Content-Type: application/xml , y el objeto Author se ha serializado a XML.

Use la pestaña Compositor para modificar la solicitud para especificar application/json en el encabezado
Accept . Ejecute la solicitud y la respuesta se formateará como JSON:

En esta captura de pantalla, puede ver que la solicitud establece un encabezado Accept: application/json y la
respuesta lo especifica como su Content-Type . El objeto Author se muestra en el cuerpo de la respuesta, en
formato JSON.
Forzar un formato determinado
Si quiere restringir los formatos de respuesta para una acción concreta, aplique el filtro [Produces] . El filtro
[Produces] especifica los formatos de respuesta para una acción o controlador específicos. Al igual que la
mayoría de los filtros, puede aplicarse a la acción, el controlador o el ámbito global.
[Produces("application/json")]
public class AuthorsController

El filtro [Produces] obligará a que todas las acciones dentro de AuthorsController devuelvan respuestas con
formato JSON, incluso si se han configurado otros formateadores para la aplicación y si el cliente ha
proporcionado un encabezado Accept que solicita un formato diferente disponible. Vea Filtros para obtener
más información, incluida la forma de aplicar filtros globales.
Formateadores de casos especiales
Algunos casos especiales se implementan mediante formateadores integrados. De forma predeterminada, los
tipos de valor devueltos string se formatearán como texto/sin formato (texto/html si se solicita a través del
encabezado Accept ). Este comportamiento se puede quitar mediante la eliminación de TextOutputFormatter .
Los formateadores deben quitarse del método Configure de Startup.cs (como se muestra a continuación). Las
acciones que tienen un tipo de valor devuelto de objeto de modelo devolverán una respuesta "204 Sin
contenido" al devolver null . Este comportamiento se puede quitar mediante la eliminación de
HttpNoContentOutputFormatter . El código siguiente quita TextOutputFormatter y HttpNoContentOutputFormatter .

services.AddMvc(options =>
{
options.OutputFormatters.RemoveType<TextOutputFormatter>();
options.OutputFormatters.RemoveType<HttpNoContentOutputFormatter>();
});

Sin TextOutputFormatter , los tipos de valor devueltos string devuelven "406 - No aceptable", por ejemplo.
Tenga en cuenta que si existe un formateador XML, dará formato a los tipos de valor devueltos string si se
quita TextOutputFormatter .
Sin HttpNoContentOutputFormatter , se da formato a los objetos nulos mediante el formateador configurado. Por
ejemplo, el formateador JSON simplemente devolverá una respuesta con un cuerpo null , mientras que el
formateador XML devolverá un elemento XML vacío con el atributo xsi:nil="true" establecido.

Asignaciones de direcciones URL de formato de respuesta


Los clientes pueden solicitar un formato determinado como parte de la dirección URL, como en la cadena de
consulta o en una parte de la ruta de acceso, o mediante el uso de una extensión de archivo de formato
específico, como .xml o .json. La asignación de la ruta de acceso de la solicitud debe especificarse en la ruta que
use la API. Por ejemplo:

[FormatFilter]
public class ProductsController
{
[Route("[controller]/[action]/{id}.{format?}")]
public Product GetById(int id)

Esta ruta permitiría especificar el formato solicitado como una extensión de archivo opcional. El atributo
[FormatFilter] comprueba la existencia del valor de formato en RouteData y asignará el formato de respuesta
al formateador adecuado cuando se cree la respuesta.

RUTA FORMATEADOR

/products/GetById/5 Formateador de salida predeterminado


RUTA FORMATEADOR

/products/GetById/5.json Formateador JSON (si está configurado)

/products/GetById/5.xml Formateador XML (si está configurado)


SignalR de ASP.NET Core
07/09/2018 • 2 minutes to read • Edit Online

Introducción
Introducción
Conceptos de servidor
Plataformas compatibles
Concentradores
HubContext
Usuarios y grupos
Publicar en Azure
Clientes
Cliente .NET
Cliente de Java
Cliente de JavaScript
API de JavaScript
WebPack y TypeScript
Configuración
Autenticación y autorización
Consideraciones de seguridad
Protocolo de concentrador MessagePack
Streaming
Diferencias entre las versiones de SignalR
Introducción a ASP.NET Core SignalR
30/07/2018 • 4 minutes to read • Edit Online

¿Qué es SignalR?
ASP.NET Core SignalR es una biblioteca de código abierto que simplifica agregar la funcionalidad a las
aplicaciones web en tiempo real. La funcionalidad web en tiempo real permite que el código de servidor para
insertar contenido a los clientes al instante.
Buenos candidatos para SignalR:
Aplicaciones que requieren actualizaciones de alta frecuencia desde el servidor. Algunos ejemplos son juegos,
redes sociales, votación, subastas, mapas y aplicaciones GPS.
Los paneles y aplicaciones de supervisión. Los ejemplos incluyen paneles empresariales, actualizaciones de
venta instantáneas o alertas de viaje.
Aplicaciones de colaboración. Las aplicaciones de pizarra y software de reuniones de equipo son ejemplos de
aplicaciones de colaboración.
Aplicaciones que requieren las notificaciones. Redes sociales, correo electrónico, chat, juegos, alertas de viaje y
muchas otras aplicaciones utilizan notificaciones.
SignalR proporciona una API para crear el 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.
Estas son algunas características de SignalR para ASP.NET Core:
Controla automáticamente la administración de conexiones.
Envía mensajes a todos los clientes conectados al mismo tiempo. Por ejemplo, un salón de chat.
Envía mensajes a clientes específicos o grupos de clientes.
Se puede ampliar para controlar el creciente tráfico.
El origen se hospeda en un SignalR repositorio de GitHub.

Transportes
SignalR admite varias técnicas para controlar las comunicaciones en tiempo real:
WebSockets
Eventos enviados por el servidor
Sondeo largo
SignalR elige automáticamente el mejor método de transporte que se encuentra dentro de las capacidades del
servidor y cliente.

Concentradores
SignalR usa hubs para la comunicación entre clientes y servidores.
Un concentrador es una canalización de alto nivel que permite que un cliente y servidor llamar a métodos entre
sí. SignalR controla el envío a través de los límites del equipo automáticamente, permitir que los clientes llamar a
métodos en el servidor y viceversa. Puede 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 basado
en JSON y un protocolo binario según MessagePack. MessagePack generalmente crea mensajes más pequeños
en comparación con JSON. Deben ser compatible con los exploradores más antiguos nivel XHR 2 para
proporcionar compatibilidad con el protocolo MessagePack.
Centros de llamar a código de cliente mediante el envío de mensajes que contienen el nombre y los parámetros
del método del lado cliente. Se deserializan los objetos que se envía como parámetros de método mediante el
protocolo configurado. El cliente intenta hacer coincidir el nombre a un método en el código del lado cliente.
Cuando el cliente encuentra una coincidencia, llama al método y le pasa los datos deserializados.

Recursos adicionales
Introducción a SignalR para ASP.NET Core
Plataformas compatibles
Concentradores
Cliente de JavaScript
Tutorial: Introducción a SignalR en ASP.NET Core
24/09/2018 • 11 minutes to read • Edit Online

En este tutorial se describen los conceptos básicos de la creación de una aplicación en tiempo real con SignalR.
Aprenderá a:
Crear una aplicación web en la que se usa SignalR en ASP.NET Core.
Crear un concentrador SignalR en el servidor.
Conectarse con el concentrador SignalR desde clientes de JavaScript.
Usar el concentrador para enviar mensajes desde cualquier cliente a todos los clientes conectados.
Al final, tendrá una aplicación de chat funcional:

Vea o descargue el código de ejemplo (cómo descargarlo).

Requisitos previos
Visual Studio
Visual Studio Code
Visual Studio para Mac
Visual Studio 2017 versión 15.8 o posterior con la carga de trabajo ASP.NET y desarrollo web
.NET Core SDK 2.1 o posterior

Crear el proyecto
Visual Studio
Visual Studio Code
Visual Studio para Mac
En el menú, seleccione Archivo > Nuevo proyecto.
En el cuadro de diálogo Nuevo proyecto, seleccione Instalado > Visual C# > Web > Aplicación web
ASP.NET Core. Asigne al proyecto el nombre SignalRChat.

Seleccione Aplicación web para crear un proyecto en el que se use Razor Pages.
Seleccione una plataforma de destino de .NET Core, seleccione ASP.NET Core 2.1 y haga clic en
Aceptar.

Agregar la biblioteca cliente de SignalR


La biblioteca de servidor de SignalR se incluye en el metapaquete Microsoft.AspNetCore.App. La biblioteca
cliente de JavaScript no se incluye automáticamente en el proyecto. En este tutorial, usará el Administrador de
bibliotecas (LibMan) para obtener la biblioteca cliente de unpkg. unpkg es una red de entrega de contenido que
puede entregar todo lo que encuentre en npm, el Administrador de paquetes Node.js.
Visual Studio
Visual Studio Code
Visual Studio para Mac
En el Explorador de soluciones, haga clic con el botón derecho en el proyecto y seleccione Agregar >
Client-Side Library (Biblioteca del lado cliente).
En el cuadro de diálogo Add Client-Side Library (Agregar biblioteca del lado cliente), en Proveedor,
seleccione unpkg.
En Biblioteca, escriba _@aspnet/signalr@1_ y seleccione la versión más reciente que no sea una versión
preliminar.

Seleccione Choose specific files (Elegir archivos específicos), expanda la carpeta dist/browser y
seleccione signalr.js y signalr.min.js.
Establezca Ubicación de destino en wwwroot/lib/signalr/ y seleccione Instalar.
LibMan crea una carpeta wwwroot/lib/signalr y copia en ella los archivos seleccionados.

Crear el concentrador SignalR


Un concentrador es una clase que actúa como una canalización general que controla la comunicación entre el
cliente y el servidor.
En la carpeta del proyecto SignalRChat, cree una carpeta Hubs.
En la carpeta Hubs, cree un archivo ChatHub.cs con el código siguiente:

using Microsoft.AspNetCore.SignalR;
using System.Threading.Tasks;

namespace SignalRChat.Hubs
{
public class ChatHub : Hub
{
public async Task SendMessage(string user, string message)
{
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
}
}

La clase ChatHub hereda de la clase Hub de SignalR. La clase Hub administra las conexiones, los grupos y
la mensajería.
Cualquier cliente conectado puede llamar al método SendMessage . Envía el mensaje recibido a todos los
clientes. El código de SignalR es asincrónico para proporcionar la máxima escalabilidad.

Configurar el proyecto para que use SignalR


El servidor de SignalR se debe configurar para que pase las solicitudes de SignalR a SignalR.
Agregue el código resaltado siguiente al archivo Startup.cs.
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
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; }

// 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.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

services.AddSignalR();
}

// 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.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseSignalR(routes =>
{
routes.MapHub<ChatHub>("/chatHub");
});
app.UseMvc();
}
}
}

Estos cambios agregan SignalR al sistema de inserción de dependencias y a la canalización de software


intermedio.
Crear el código de cliente de SignalR
Reemplace el contenido de Pages/Index.cshtml con el código siguiente:

@page
<div class="container">
<div class="row">&nbsp;</div>
<div class="row">
<div class="col-6">&nbsp;</div>
<div class="col-6">
User..........<input type="text" id="userInput" />
<br />
Message...<input type="text" id="messageInput" />
<input type="button" id="sendButton" value="Send Message" />
</div>
</div>
<div class="row">
<div class="col-12">
<hr />
</div>
</div>
<div class="row">
<div class="col-6">&nbsp;</div>
<div class="col-6">
<ul id="messagesList"></ul>
</div>
</div>
</div>
<script src="~/lib/signalr/dist/browser/signalr.js"></script>
<script src="~/js/chat.js"></script>

El código anterior:
Crea cuadros de texto para el nombre y el mensaje de texto, y un botón de envío.
Crea una lista con id="messagesList" para mostrar los mensajes que se reciben desde el concentrador
SignalR.
Incluye las referencias de script en SignalR y el código de aplicación de chat.js que se va a crear en el
paso siguiente.
En la carpeta wwwroot/js, cree un archivo chat.js con el código siguiente:
"use strict";

var connection = new signalR.HubConnectionBuilder().withUrl("/chatHub").build();

connection.on("ReceiveMessage", function (user, message) {


var msg = message.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
var encodedMsg = user + " says " + msg;
var li = document.createElement("li");
li.textContent = encodedMsg;
document.getElementById("messagesList").appendChild(li);
});

connection.start().catch(function (err) {
return console.error(err.toString());
});

document.getElementById("sendButton").addEventListener("click", function (event) {


var user = document.getElementById("userInput").value;
var message = document.getElementById("messageInput").value;
connection.invoke("SendMessage", user, message).catch(function (err) {
return console.error(err.toString());
});
event.preventDefault();
});

El código anterior:
Crea e inicia una conexión.
Agrega al botón de envío un controlador que envía mensajes al concentrador.
Agrega al objeto de conexión un controlador que recibe mensajes desde el concentrador y los agrega a
la lista.

Ejecutar la aplicación
Visual Studio
Visual Studio Code
Visual Studio para Mac
Presione CTRL+F5 para ejecutar la aplicación sin depurar.
Copie la dirección URL de la barra de direcciones, abra otra instancia o pestaña del explorador, y pegue la
dirección URL en la barra de direcciones.
Elija cualquier explorador, escriba un nombre y un mensaje, y haga clic en el botón Enviar.
El nombre y el mensaje se muestran en ambas páginas al instante.
TIP
Si la aplicación no funciona, abra las herramientas para desarrolladores del explorador (F12) y vaya a la consola. Es posible
que vea errores relacionados con el código HTML y JavaScript. Por ejemplo, suponga que coloca signalr.js en una carpeta
distinta a la indicada. En ese caso, la referencia a ese archivo no funcionará y verá un error 404 en la consola.

Pasos siguientes
Si quiere que los clientes se conecten a una aplicación de SignalR desde otros dominios, tendrá que habilitar el
uso compartido de recursos entre orígenes (CORS ). Para obtener más información, vea Uso compartido de
recursos entre orígenes.
Para obtener más información sobre SignalR, los concentradores y los clientes de JavaScript, vea estos recursos:
Introducción a SignalR para ASP.NET Core
Uso de concentradores de SignalR para ASP.NET Core
Cliente de JavaScript de SignalR para ASP.NET Core
Usar concentradores de SignalR para ASP.NET Core
14/09/2018 • 9 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 le 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 los métodos
que se llaman desde el servidor. SignalR se ocupa de todo en segundo plano que hace posible la comunicación de
servidor de cliente y servidor a cliente en tiempo real.

Configurar los concentradores de 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 ASP.NET Core, configurar las rutas de SignalR mediante
una llamada a app.UseSignalR en el Startup.Configure método.

app.UseSignalR(route =>
{
route.MapHub<ChatHub>("/chathub");
});

Crear y usar concentradores


Crear un centro 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 sus
parámetros y valores devueltos.

El objeto de contexto
El Hub clase tiene un Context propiedad que contiene las siguientes propiedades con información sobre la
conexión:

PROPERTY DESCRIPCIÓN

ConnectionId Obtiene el identificador único para la conexión, asignada por


SignalR. Hay un identificador de conexión para cada conexión.

UserIdentifier Obtiene el useridentifier. De forma predeterminada, usa


SignalR el ClaimTypes.NameIdentifier desde el
ClaimsPrincipal asociado a la conexión como el
identificador de usuario.

User Obtiene el ClaimsPrincipal asociada con el usuario actual.

Items Obtiene una colección de pares clave-valor que se puede usar


para compartir datos dentro del ámbito de esta conexión. Se
pueden almacenar datos en esta colección y conservará para la
conexión entre las invocaciones de método de concentrador.
PROPERTY DESCRIPCIÓN

Features Obtiene la colección de las características disponibles en la


conexión. Por ahora, esta colección no es necesario en la
mayoría de los escenarios, por lo que no se documentan
detalladamente todavía.

ConnectionAborted Obtiene un CancellationToken que le notifica cuando se


anula la conexión.

Hub.Context También contiene los métodos siguientes:

MÉTODO DESCRIPCIÓN

GetHttpContext Devuelve el HttpContext para la conexión, o null si la


conexión no está asociada a una solicitud HTTP. Para las
conexiones HTTP, puede usar este método para obtener
información como los encabezados HTTP y las cadenas de
consulta.

Abort Anula la conexión.

El objeto de los clientes


El Hub clase tiene un Clients propiedad que contiene las siguientes propiedades 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

Hub.Clients También contiene los métodos siguientes:

MÉTODO DESCRIPCIÓN

AllExcept Llama a un método en todos los clientes conectados, excepto


las conexiones especificadas

Client Llama a un método en un cliente específico conectado

Clients Llame a un método específicos clientes conectados

Group Llama a un método a todas las conexiones del grupo


especificado

GroupExcept Llama a un método a todas las conexiones en el grupo


especificado, excepto las conexiones especificadas
MÉTODO DESCRIPCIÓN

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 en las tablas anteriores devuelve un objeto con un SendAsync método. El SendAsync
método permite proporcionar el nombre y los parámetros del método para llamar al cliente.

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 invoca el método de
concentrador. El SendMessageToGroups método envía un mensaje a los grupos que se almacenan 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);
}

Concentradores fuertemente tipados


Una desventaja de utilizar SendAsync es que se basa en una cadena mágica para especificar que se llame al método
de cliente. Esto deja el código abierto a los errores de tiempo de ejecución si el nombre del método está mal escrito
o no aparece en el cliente.
Una alternativa al uso SendAsync es asignar rigurosamente los Hub con Hub<T>. En el ejemplo siguiente, la
ChatHub métodos de cliente se han extraído alejar en una interfaz denominada IChatClient .

public interface IChatClient


{
Task ReceiveMessage(string user, string message);
Task ReceiveMessage(string message);
}

Esta interfaz puede usarse para refactorizar anterior ChatHub ejemplo.


public class StronglyTypedChatHub : Hub<IChatClient>
{
public async Task SendMessage(string user, string message)
{
await Clients.All.ReceiveMessage(user, message);
}

public Task SendMessageToCaller(string message)


{
return Clients.Caller.ReceiveMessage(message);
}
}

Uso de Hub<IChatClient> habilita la comprobación de tiempo de compilación de los métodos de cliente. Esto evita
problemas causados por usar cadenas mágicas, ya que Hub<T> solo puede proporcionar acceso a los métodos
definidos en la interfaz.
Usar fuertemente tipado Hub<T> deshabilita la posibilidad de usar SendAsync .

Controlar los 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 los 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 promesa de JavaScript. Cuando el cliente recibe un error con un
controlador asociado a la promesa mediante catch , se invoca y pasado como un 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
Plataformas compatibles con ASP.NET Core SignalR
18/07/2018 • 2 minutes to read • Edit Online

Requisitos del sistema de servidor


SignalR para ASP.NET Core es compatible con ASP.NET Core es compatible con cualquier plataforma de
servidor.

Requisitos del sistema cliente


Compatibilidad con exploradores
SignalR para ASP.NET Core JavaScript cliente es compatible con 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 versiones posteriores. Otros transportes se admiten en
todas las plataformas.
Usar concentradores de SignalR para ASP.NET
Core
14/09/2018 • 9 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 le 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 los métodos que se llaman desde el servidor. SignalR se ocupa de todo en segundo plano que hace
posible la comunicación de servidor de cliente y servidor a cliente en tiempo real.

Configurar los concentradores de 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 ASP.NET Core, configurar las rutas de SignalR
mediante una llamada a app.UseSignalR en el Startup.Configure método.

app.UseSignalR(route =>
{
route.MapHub<ChatHub>("/chathub");
});

Crear y usar concentradores


Crear un centro 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 sus parámetros y valores devueltos.

El objeto de contexto
El Hub clase tiene un Context propiedad que contiene las siguientes propiedades con información sobre la
conexión:

PROPERTY DESCRIPCIÓN

ConnectionId Obtiene el identificador único para la conexión, asignada


por SignalR. Hay un identificador de conexión para cada
conexión.

UserIdentifier Obtiene el useridentifier. De forma predeterminada, usa


SignalR el ClaimTypes.NameIdentifier desde el
ClaimsPrincipal asociado a la conexión como el
identificador de usuario.

User Obtiene el ClaimsPrincipal asociada con el usuario


actual.

Items Obtiene una colección de pares clave-valor que se puede


usar para compartir datos dentro del ámbito de esta
conexión. Se pueden almacenar datos en esta colección y
conservará para la conexión entre las invocaciones de
método de concentrador.
PROPERTY DESCRIPCIÓN

Features Obtiene la colección de las características disponibles en la


conexión. Por ahora, esta colección no es necesario en la
mayoría de los escenarios, por lo que no se documentan
detalladamente todavía.

ConnectionAborted Obtiene un CancellationToken que le notifica cuando se


anula la conexión.

Hub.Context También contiene los métodos siguientes:

MÉTODO DESCRIPCIÓN

GetHttpContext Devuelve el HttpContext para la conexión, o null si la


conexión no está asociada a una solicitud HTTP. Para las
conexiones HTTP, puede usar este método para obtener
información como los encabezados HTTP y las cadenas de
consulta.

Abort Anula la conexión.

El objeto de los clientes


El Hub clase tiene un Clients propiedad que contiene las siguientes propiedades 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

Hub.Clients También contiene los métodos siguientes:

MÉTODO DESCRIPCIÓN

AllExcept Llama a un método en todos los clientes conectados,


excepto las conexiones especificadas

Client Llama a un método en un cliente específico conectado

Clients Llame a un método específicos clientes conectados

Group Llama a un método a todas las conexiones del grupo


especificado

GroupExcept Llama a un método a todas las conexiones en el grupo


especificado, excepto las conexiones especificadas
MÉTODO DESCRIPCIÓN

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 en las tablas anteriores devuelve un objeto con un SendAsync método. El
SendAsync método permite proporcionar el nombre y los parámetros del método para llamar al cliente.

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 invoca el método de
concentrador. El SendMessageToGroups método envía un mensaje a los grupos que se almacenan 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);
}

Concentradores fuertemente tipados


Una desventaja de utilizar SendAsync es que se basa en una cadena mágica para especificar que se llame al
método de cliente. Esto deja el código abierto a los errores de tiempo de ejecución si el nombre del método
está mal escrito o no aparece en el cliente.
Una alternativa al uso SendAsync es asignar rigurosamente los Hub con Hub<T>. En el ejemplo siguiente, la
ChatHub métodos de cliente se han extraído alejar en una interfaz denominada IChatClient .

public interface IChatClient


{
Task ReceiveMessage(string user, string message);
Task ReceiveMessage(string message);
}

Esta interfaz puede usarse para refactorizar anterior ChatHub ejemplo.


public class StronglyTypedChatHub : Hub<IChatClient>
{
public async Task SendMessage(string user, string message)
{
await Clients.All.ReceiveMessage(user, message);
}

public Task SendMessageToCaller(string message)


{
return Clients.Caller.ReceiveMessage(message);
}
}

Uso de Hub<IChatClient> habilita la comprobación de tiempo de compilación de los métodos de cliente. Esto
evita problemas causados por usar cadenas mágicas, ya que Hub<T> solo puede proporcionar acceso a los
métodos definidos en la interfaz.
Usar fuertemente tipado Hub<T> deshabilita la posibilidad de usar SendAsync .

Controlar los 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 los 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 promesa de JavaScript. Cuando el cliente recibe un error
con un controlador asociado a la promesa mediante catch , se invoca y pasado como un 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
Enviar mensajes desde fuera de un concentrador
11/09/2018 • 2 minutes to read • Edit Online

Por Mikael Mengistu


El centro de SignalR es la abstracción principal para enviar mensajes a los clientes conectados al servidor de
SignalR. También es posible enviar mensajes desde otros lugares de la aplicación mediante el IHubContext service.
En este artículo se 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 mediante la inserción de
dependencia. Puede insertar una instancia de IHubContext en un controlador, middleware u otro servicio de
inserción de dependencias. Utilice la instancia para enviar mensajes a los clientes.

NOTE
Esto difiere de ASP.NET 4.x SignalR que usa GlobalHost para proporcionar acceso a la IHubContext . ASP.NET Core tiene un
marco de inserción de dependencia que elimina la necesidad de este singleton global.

Inyectar una instancia de IHubContext en un controlador


Puede insertar una instancia de IHubContext en un controlador al agregarlo 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 centro 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 en middleware


Acceso a la IHubContext dentro de la canalización de middleware siguiente manera:
app.Use(next => async (context) =>
{
var hubContext = context.RequestServices
.GetRequiredService<IHubContext<MyHub>>();
//...
});

NOTE
Cuando se llaman a métodos de concentrador desde fuera de la Hub clase, no hay ningún autor de llamada asociada con 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 en SignalR
18/07/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, 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 su escritorio, así como su teléfono. Cada dispositivo tiene una
conexión SignalR independiente, pero que están todos los asociados con el mismo usuario. Si se envía un
mensaje al usuario, todas las conexiones asociadas con la que el usuario recepción el mensaje. El identificador de
usuario para una conexión puede tener acceso a la Context.UserIdentifier propiedad en el centro.
Enviar un mensaje a un usuario específico, pasando 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 sus servicios personalizados de SignalR.

Grupos en SignalR
Un grupo es una colección de conexiones asociado con 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 sala puede representarse como
un grupo. Las conexiones se pueden sumar o quitan 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 en 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
Los nombres de grupo distinguen entre mayúsculas y minúsculas.

Recursos relacionados
Introducción
Concentradores
Publicar en Azure
Publicar un ASP.NET Core SignalR aplicación a una
aplicación Web de Azure
29/09/2018 • 3 minutes to read • Edit Online

Azure Web App es un Microsoft la informática en nube plataforma de servicio para hospedar aplicaciones web,
incluido ASP.NET Core.

NOTE
Este artículo se refiere a la publicación de una aplicación ASP.NET Core SignalR desde Visual Studio. Visite SignalR service
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 en 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 ASP.NET Core en Azure con 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 App Service 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 para 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 Azure Web App 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 de la aplicación se {nombre de la aplicación}. azurewebsites NET.
Por ejemplo, una aplicación denominada SignalRChattR tiene una dirección URL similar a
https://signalrchattr.azurewebsites.net .

Si se produce un error HTTP 502.2, consulte versión de vista previa de la implementación de ASP.NET Core en
Azure App Service para resolverlo.

Configuración de aplicación web de SignalR


ASP.NET Core SignalR aplicaciones que se publican como una aplicación Web de Azure debe tener afinidad
ARR habilitado. WebSockets debe habilitarse para permitir que el transporte de WebSockets funcione.
En el portal de Azure, vaya a configuración de la aplicación para la aplicación web. Establecer WebSockets a
eny compruebe afinidad ARR es en.

WebSockets y otros transportes están limitados según el Plan de App Service.

Recursos relacionados
Publicar una aplicación ASP.NET Core en Azure con herramientas de línea de comandos
Publicar una aplicación ASP.NET Core en Azure con Visual Studio
Hospedar e implementar aplicaciones de la versión preliminar de ASP.NET Core en Azure
Cliente ASP.NET Core SignalR JavaScript
21/09/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 JavaScript de SignalR se entrega como un npm paquete. Si usa Visual Studio, ejecute
npm install desde el Package Manager Console mientras se encuentre en la carpeta raíz. Para Visual Studio
Code, 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 bajo el wwwroot\lib carpeta. Copia el signalr.js del archivo a la wwwroot\lib\signalr
carpeta.

Usar al cliente de JavaScript de SignalR


Referencia del cliente de JavaScript de SignalR en el <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 de origen cruzado


Normalmente, exploradores de carga de 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 lea datos confidenciales de otro sitio, las conexiones de origen cruzado
están deshabilitados de forma predeterminada. Para permitir que una solicitud de origen cruzado, habilitarlo en el
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 JavaScript llamar a métodos públicos en concentradores a través de la invocar método de la
HubConnection. El invoke método acepta dos argumentos:
El nombre del método de concentrador. En el ejemplo siguiente, que es el nombre del método del
concentrador SendMessage .
Los argumentos definidos en el método de concentrador. En el ejemplo siguiente, que es el nombre del
argumento message .

connection.invoke("SendMessage", user, message).catch(err => console.error(err.toString()));

Llamar a métodos cliente desde el concentrador


Para recibir mensajes desde el centro, defina un método mediante el en método de la HubConnection .
El nombre del método de cliente de JavaScript. En el ejemplo siguiente, que es el nombre del método
ReceiveMessage .
Argumentos que del concentrador 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 se llama al 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 qué método de cliente para llamar a haciendo coincidir el nombre del método y los argumentos
definan en SendAsync y connection.on .

NOTE
Como práctica recomendada, llame a la iniciar método en el HubConnection después on . Si lo hace, garantiza que los
controladores registrados antes de que se reciben los mensajes.

Registro y control de errores


Cadena de un catch método al final de la start método para controlar los errores del lado cliente. Use
console.error para errores de salida a la consola del explorador.

connection.start().catch(err => console.error(err.toString()));

Seguimiento de registro del lado cliente instalación 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. Los niveles de registro disponibles son los siguientes:
signalR.LogLevel.Error – Mensajes de error. Registros Error solo los mensajes.
signalR.LogLevel.Warning – Mensajes de advertencia acerca de posibles errores. Los registros de Warning , y
Error mensajes.
signalR.LogLevel.Information – Mensajes de estado sin errores. Los 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 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 adicionales
Referencia de la API de JavaScript
Concentradores
Cliente .NET
Publicar en Azure
Habilitar solicitudes entre orígenes (CORS ) en ASP.NET Core
Cliente de .NET de ASP.NET Core SignalR
13/09/2018 • 4 minutes to read • Edit Online

La biblioteca de cliente de ASP.NET Core SignalR .NET le permite comunicarse con los concentradores de
SignalR desde aplicaciones de. NET.

NOTE
Xamarin tiene requisitos especiales para la versión de Visual Studio. Para obtener más información, consulte 2.1.1 de cliente
de SignalR en Xamarin.

Vea o descargue el código de ejemplo (cómo descargarlo)


El ejemplo de código en este artículo es una aplicación WPF que usa al cliente de .NET de ASP.NET Core
SignalR.

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 de SignalR. Para instalar la biblioteca de cliente, ejecute el siguiente comando en el Package
Manager Console ventana:

Install-Package Microsoft.AspNetCore.SignalR.Client

Conectarse a un concentrador
Para establecer una conexión, cree un HubConnectionBuilder y llamar a Build . Durante la compilación de una
conexión se pueden configurar la URL del centro, protocolo, tipo de transporte, nivel de registro, los encabezados
y otras opciones. Configurar las opciones necesarias mediante la inserción de cualquiera de los
HubConnectionBuilder métodos en Build . Iniciar la conexión con StartAsync .
using System;
using System.Threading.Tasks;
using System.Windows;
using Microsoft.AspNetCore.SignalR.Client;

namespace SignalRChatClient
{
public partial class MainWindow : Window
{
HubConnection connection;
public MainWindow()
{
InitializeComponent();

connection = new HubConnectionBuilder()


.WithUrl("http://localhost:53353/ChatHub")
.Build();

connection.Closed += async (error) =>


{
await Task.Delay(new Random().Next(0,5) * 1000);
await connection.StartAsync();
};
}

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

Controlar la pérdida de conexión


Use el Closed eventos para responder a una pérdida de conexión. Por ejemplo, es posible que desee automatizar
la reconexión.
El Closed evento requiere un delegado que devuelve un Task , lo que permite ejecutar sin usar código
asincrónico async void . Para cumplir con la firma del delegado en un Closed controlador de eventos que se
ejecuta de forma sincrónica, devolver Task.CompletedTask :

connection.Closed += (error) => {


// Do your close logic.
return Task.CompletedTask;
};

La razón principal para el soporte para asincronía es por lo que puede reiniciar la conexión. A partir de una
conexión es una acción asincrónica.
En un Closed controlador que se reinicia la conexión, considere la posibilidad de esperar a cierto retraso
aleatorio evitar la sobrecarga del servidor, como se muestra en el ejemplo siguiente:

connection.Closed += async (error) =>


{
await Task.Delay(new Random().Next(0,5) * 1000);
await connection.StartAsync();
};

Llamar a métodos de concentrador de cliente


InvokeAsync llama a métodos en el concentrador. Pase el nombre del método de concentrador y los argumentos
definidos en el método de concentrador a InvokeAsync . SignalR es asincrónica, así que use async y await al
realizar las llamadas.

await connection.InvokeAsync("SendMessage",
userTextBox.Text, messageTextBox.Text);

Llamar a métodos cliente desde el concentrador


Definir métodos que el centro de llamadas utilizando 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 se llama al 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
adecuada que 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
Cliente de SignalR Java de ASP.NET Core
21/09/2018 • 3 minutes to read • Edit Online

Por Mikael Mengistu


El cliente de Java que permite conectarse a un servidor de ASP.NET Core SignalR desde el código de Java,
incluidas las aplicaciones Android. Al igual que el cliente JavaScript y el cliente .NET, el cliente de Java que le
permite recibir y enviar mensajes a un concentrador en tiempo real. El cliente de Java está disponible en ASP.NET
Core 2.2 y posteriores.
En este artículo hace referencia a la aplicación de consola de Java de ejemplo usa al cliente de SignalR Java.
Vea o descargue el código de ejemplo (cómo descargarlo)

Instale el paquete de cliente de SignalR Java


El signalr-0.1.0 -preview2 -35174 archivo JAR permite a los clientes para conectarse a los concentradores de
SignalR. Para buscar el número de versión de archivo JAR más reciente, consulte el los resultados de búsqueda de
Maven.
Si usa Gradle, agregue la siguiente línea a la dependencies sección de su build.gradle archivo:

implementation 'com.microsoft.aspnet:signalr:0.1.0-preview2-35174'

Si con Maven, agregue las siguientes líneas dentro de la <dependencies> elemento de su pom.xml archivo:

<dependency>
<groupId>com.microsoft.aspnet</groupId>
<artifactId>signalr</artifactId>
<version>0.1.0-preview2-35174</version>
</dependency>

Conectarse a un concentrador
Para establecer un HubConnection , el HubConnectionBuilder se debe usar. El nivel de registro y la dirección URL del
centro se puede configurar durante la compilación de una conexión. Configurar las opciones necesarias mediante
una llamada a cualquiera de los HubConnectionBuilder métodos antes de build . Iniciar la conexión con start .

HubConnection hubConnection = new HubConnectionBuilder()


.withUrl(input)
.configureLogging(LogLevel.Information)
.build();

Llamar a métodos de concentrador de cliente


Una llamada a send invoca un método de concentrador. Pase el nombre del método de concentrador y los
argumentos definidos en el método de concentrador a send .

hubConnection.send("Send", input);
Llamar a métodos cliente desde el concentrador
Use hubConnection.on para definir métodos en el cliente que se puede llamar la central. Definir los métodos
después de compilar, pero antes de iniciar la conexión.

hubConnection.on("Send", (message) -> {


System.out.println("New Message: " + message);
}, String.class);

Limitaciones conocidas
Se trata de una versión preliminar del cliente de Java. Hay muchas características que aún no se admiten. Para las
versiones futuras, están trabajando los huecos siguientes:
Solo los tipos primitivos se pueden aceptar como parámetros y tipos de valor devuelto.
Las API son sincrónicas.
Solo el tipo de llamada de "Envío" se admite en este momento. No se admiten "Invocar" y la transmisión por
secuencias de valores devueltos.
Se admite solo el protocolo JSON.
Se admite sólo el transporte de WebSockets.

Recursos adicionales
Referencia de la API de Java
Usar concentradores en ASP.NET Core SignalR
Cliente ASP.NET Core SignalR JavaScript
Publicar un ASP.NET Core SignalR aplicación a aplicación Web de Azure
Cliente ASP.NET Core SignalR JavaScript
21/09/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 JavaScript de SignalR se entrega como un npm paquete. Si usa Visual Studio,
ejecute npm install desde el Package Manager Console mientras se encuentre en la carpeta raíz. Para
Visual Studio Code, 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 bajo el wwwroot\lib carpeta. Copia el signalr.js del archivo a la
wwwroot\lib\signalr carpeta.

Usar al cliente de JavaScript de SignalR


Referencia del cliente de JavaScript de SignalR en el <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 de origen cruzado


Normalmente, exploradores de carga de 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 lea datos confidenciales de otro sitio, las conexiones de origen
cruzado están deshabilitados de forma predeterminada. Para permitir que una solicitud de origen cruzado,
habilitarlo en el 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 JavaScript llamar a métodos públicos en concentradores a través de la invocar método de la
HubConnection. El invoke método acepta dos argumentos:
El nombre del método de concentrador. En el ejemplo siguiente, que es el nombre del método del
concentrador SendMessage .
Los argumentos definidos en el método de concentrador. En el ejemplo siguiente, que es el nombre del
argumento message .

connection.invoke("SendMessage", user, message).catch(err => console.error(err.toString()));

Llamar a métodos cliente desde el concentrador


Para recibir mensajes desde el centro, defina un método mediante el en método de la HubConnection .
El nombre del método de cliente de JavaScript. En el ejemplo siguiente, que es el nombre del método
ReceiveMessage .
Argumentos que del concentrador 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 se llama al 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 qué método de cliente para llamar a haciendo coincidir el nombre del método y los
argumentos definan en SendAsync y connection.on .

NOTE
Como práctica recomendada, llame a la iniciar método en el HubConnection después on . Si lo hace, garantiza que los
controladores registrados antes de que se reciben los mensajes.

Registro y control de errores


Cadena de un catch método al final de la start método para controlar los errores del lado cliente. Use
console.error para errores de salida a la consola del explorador.

connection.start().catch(err => console.error(err.toString()));

Seguimiento de registro del lado cliente instalación 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. Los niveles de registro disponibles son los siguientes:
signalR.LogLevel.Error – Mensajes de error. Registros Error solo los mensajes.
signalR.LogLevel.Warning – Mensajes de advertencia acerca de posibles errores. Los registros de Warning ,
y Error mensajes.
signalR.LogLevel.Information – Mensajes de estado sin errores. Los 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 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 adicionales
Referencia de la API de JavaScript
Concentradores
Cliente .NET
Publicar en Azure
Habilitar solicitudes entre orígenes (CORS ) en ASP.NET Core
Uso de SignalR de ASP.NET Core con TypeScript y
Webpack
23/07/2018 • 20 minutes to read • Edit Online

Por Sébastien Sougnez y Scott Addie


Webpack permite a los desarrolladores agrupar y compilar los recursos del lado cliente de una aplicación web. En
este tutorial se describe el uso de Webpack en una aplicación web de SignalR de ASP.NET Core cuyo cliente está
escrito en TypeScript.
En este tutorial aprenderá a:
Aplicación de scaffolding a una aplicación de inicio de SignalR de ASP.NET Core
Configuración del cliente TypeScript de SignalR
Configuración de una canalización de compilación mediante Webpack
Configuración del servidor de SignalR
Habilitar la comunicación entre cliente y servidor
Vea o descargue el código de ejemplo (cómo descargarlo)

Requisitos previos
Instale el software siguiente:
Visual Studio
CLI de .NET Core
.NET Core SDK 2.1 o posterior
Node.js con npm
Visual Studio 2017 versión 15.7.3 o posterior con la carga de trabajo ASP.NET y desarrollo web

Creación de la aplicación web ASP.NET Core


Visual Studio
CLI de .NET Core
Configure Visual Studio para buscar npm en la variable de entorno PATH. De forma predeterminada, Visual
Studio usa la versión de npm que se encuentra en su directorio de instalación. Siga estas instrucciones en Visual
Studio:
1. Vaya a Herramientas > Opciones > Proyectos y soluciones > Administración de paquetes web >
Herramientas web externas.
2. Seleccione la entrada $ (PATH ) en la lista. Haga clic en la flecha arriba para mover la entrada a la segunda
posición de la lista. Por otro lado, la primera entrada hace referencia a los paquetes del proyecto local.
Se ha completado la configuración de Visual Studio. Es el momento de crear el proyecto.
1. Use la opción de menú Archivo > Nuevo > Proyecto y seleccione la plantilla Aplicación web ASP.NET
Core.
2. Asigne el nombre SignalRWebPack al proyecto y haga clic en el botón Aceptar.
3. Seleccione .NET Core en la lista desplegable de plataforma de destino y ASP.NET Core 2.1 en la lista
desplegable del selector de plataforma. Seleccione la plantilla Vacía y haga clic en el botón Aceptar.

Configuración de Webpack y TypeScript


Los pasos siguientes permiten configurar la conversión de TypeScript a JavaScript y la agrupación de los recursos
del lado cliente.
1. Ejecute el comando siguiente en la raíz del proyecto para crear un archivo package.json:

npm init -y

2. Agregue la propiedad resaltada al archivo package.json:

{
"name": "SignalRWebPack",
"version": "1.0.0",
"private": true,
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}

Si establece la propiedad private en true , evitará las advertencias de la instalación de paquetes en el


paso siguiente.
3. Instale los paquetes npm necesarios. Ejecute el comando siguiente desde la raíz del proyecto:
npm install -D -E clean-webpack-plugin@0.1.19 css-loader@0.28.11 html-webpack-plugin@3.2.0 mini-css-
extract-plugin@0.4.0 ts-loader@4.4.1 typescript@2.9.2 webpack@4.12.0 webpack-cli@3.0.6

Algunos detalles del comando para tener en cuenta:


En cada nombre de paquete, un número de versión sigue al signo @ . npm instala esas versiones de
paquete específicas.
La opción -E deshabilita el comportamiento predeterminado de npm de escribir operadores de
intervalo de versionamiento semántico en package.json. Por ejemplo, se usa "webpack": "4.12.0" en
lugar de "webpack": "^4.12.0" . Esta opción impide actualizaciones no deseadas a versiones más
recientes del paquete.
Vea la documentación oficial de npm-install para obtener más detalles.
4. Reemplace la propiedad scripts del archivo package.json por el fragmento de código siguiente:

"scripts": {
"build": "webpack --mode=development --watch",
"release": "webpack --mode=production",
"publish": "npm run release && dotnet publish -c Release"
},

Más detalles sobre los scripts:


build : agrupa los recursos del lado cliente en modo de desarrollo y supervisa los cambios del archivo.
El monitor de archivos hace que la agrupación se vuelva a generar cada vez que cambia un archivo del
proyecto. La opción mode deshabilita las optimizaciones de producción, como la agitación del árbol y la
minificación. Use build únicamente durante el desarrollo.
release : agrupa los recursos del lado cliente en modo de producción.
publish : ejecuta el script release para agrupar los recursos del lado cliente en modo de producción.
Llama al comando publish de la CLI de .NET Core para publicar la aplicación.
5. Cree un archivo denominado webpack.config.js, en la raíz del proyecto, con el contenido siguiente:
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const CleanWebpackPlugin = require("clean-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

module.exports = {
entry: "./src/index.ts",
output: {
path: path.resolve(__dirname, "wwwroot"),
filename: "[name].[chunkhash].js",
publicPath: "/"
},
resolve: {
extensions: [".js", ".ts"]
},
module: {
rules: [
{
test: /\.ts$/,
use: "ts-loader"
},
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, "css-loader"]
}
]
},
plugins: [
new CleanWebpackPlugin(["wwwroot/*"]),
new HtmlWebpackPlugin({
template: "./src/index.html"
}),
new MiniCssExtractPlugin({
filename: "css/[name].[chunkhash].css"
})
]
};

El archivo anterior configura la compilación de Webpack. Algunos detalles de configuración para tener en
cuenta:
La propiedad output invalida el valor predeterminado de dist. En su lugar, la agrupación se genera en el
directorio wwwroot.
La matriz resolve.extensions incluye .js para importar el código JavaScript cliente de SignalR.
6. Cree un directorio src en la raíz del proyecto. Su función es almacenar los activos del lado cliente del
proyecto.
7. Cree src/index.html con el contenido siguiente.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>ASP.NET Core SignalR</title>
</head>
<body>
<div id="divMessages" class="messages">
</div>
<div class="input-zone">
<label id="lblMessage" for="tbMessage">Message:</label>
<input id="tbMessage" class="input-zone-input" type="text" />
<button id="btnSend">Send</button>
</div>
</body>
</html>

El código HTML anterior define el marcado reutilizable de la página principal.


8. Cree un directorio src/css. Su objetivo es almacenar los archivos .css del proyecto.
9. Cree src/css/main.css con el contenido siguiente:

*, *::before, *::after {
box-sizing: border-box;
}

html, body {
margin: 0;
padding: 0;
}

.input-zone {
align-items: center;
display: flex;
flex-direction: row;
margin: 10px;
}

.input-zone-input {
flex: 1;
margin-right: 10px;
}

.message-author {
font-weight: bold;
}

.messages {
border: 1px solid #000;
margin: 10px;
max-height: 300px;
min-height: 300px;
overflow-y: auto;
padding: 5px;
}

El archivo main.css anterior aplica estilo a la aplicación.


10. Cree src/tsconfig.json con el contenido siguiente:
{
"compilerOptions": {
"target": "es5"
}
}

El código anterior configura el compilador de TypeScript para generar JavaScript compatible con
ECMAScript 5.
11. Cree src/index.ts con el contenido siguiente:

import "./css/main.css";

const divMessages: HTMLDivElement = document.querySelector("#divMessages");


const tbMessage: HTMLInputElement = document.querySelector("#tbMessage");
const btnSend: HTMLButtonElement = document.querySelector("#btnSend");
const username = new Date().getTime();

tbMessage.addEventListener("keyup", (e: KeyboardEvent) => {


if (e.keyCode === 13) {
send();
}
});

btnSend.addEventListener("click", send);

function send() {
}

El elemento TypeScript anterior recupera las referencias a elementos DOM y adjunta dos controladores de
eventos:
keyup : este evento se desencadena cuando el usuario escribe algo en el cuadro de texto identificado
como tbMessage . La función send se llama cuando el usuario presiona la tecla Entrar.
click : este evento se desencadena cuando el usuario clic en el botón Enviar. Se llama a la función
send .

Configuración de la aplicación ASP.NET Core


1. El código proporcionado en el método Startup.Configure muestra Hello World!. Reemplace la llamada al
método app.Run por las llamadas a UseDefaultFiles y UseStaticFiles.

app.UseDefaultFiles();
app.UseStaticFiles();

El código anterior permite que el servidor busque y proporcione el archivo index.html, con independencia
de que el usuario escriba su dirección URL completa o la dirección URL raíz de la aplicación web.
2. Llame a AddSignalR en el método Startup.ConfigureServices . Esto permite agregar los servicios SignalR
al proyecto.

services.AddSignalR();

3. Asigne una ruta /hub al concentrador ChatHub . Agregue las líneas siguientes al final del método
Startup.Configure :
app.UseSignalR(options =>
{
options.MapHub<ChatHub>("/hub");
});

4. Cree un directorio denominado Hubs en la raíz del proyecto. Su objetivo es almacenar el concentrador de
SignalR, que se crea en el paso siguiente.
5. Cree el concentrador Hubs/ChatHub.cs con el código siguiente:

using Microsoft.AspNetCore.SignalR;
using System.Threading.Tasks;

namespace SignalRWebPack.Hubs
{
public class ChatHub : Hub
{
}
}

6. Agregue el código siguiente en la parte superior del archivo Startup.cs para resolver la referencia a
ChatHub :

using SignalRWebPack.Hubs;

Habilitar la comunicación entre cliente y servidor


Actualmente, en la aplicación se muestra un formulario simple para enviar mensajes. Al intentar hacer algo no
sucede nada. El servidor está escuchando en una ruta específica, pero no hace nada con los mensajes enviados.
1. Ejecute el comando siguiente en la raíz del proyecto:

npm install @aspnet/signalr

El comando anterior instala el cliente TypeScript de SignalR, que permite al cliente enviar mensajes al
servidor.
2. Agregue el código resaltado al archivo src/index.ts:
import "./css/main.css";
import * as signalR from "@aspnet/signalr";

const divMessages: HTMLDivElement = document.querySelector("#divMessages");


const tbMessage: HTMLInputElement = document.querySelector("#tbMessage");
const btnSend: HTMLButtonElement = document.querySelector("#btnSend");
const username = new Date().getTime();

const connection = new signalR.HubConnectionBuilder()


.withUrl("/hub")
.build();

connection.start().catch(err => document.write(err));

connection.on("messageReceived", (username: string, message: string) => {


let m = document.createElement("div");

m.innerHTML =
`<div class="message__author">${username}</div><div>${message}</div>`;

divMessages.appendChild(m);
divMessages.scrollTop = divMessages.scrollHeight;
});

tbMessage.addEventListener("keyup", (e: KeyboardEvent) => {


if (e.keyCode === 13) {
send();
}
});

btnSend.addEventListener("click", send);

function send() {
}

El código anterior admite la recepción de mensajes desde el servidor. La clase HubConnectionBuilder crea
un generador para configurar la conexión al servidor. La función withUrl configura la dirección URL del
concentrador.
SignalR permite el intercambio de mensajes entre un cliente y un servidor. Cada mensaje tiene un nombre
específico. Por ejemplo, puede haber mensajes con el nombre messageReceived que ejecuten la lógica
responsable de mostrar el mensaje nuevo en la zona de mensajes. La escucha a un mensaje concreto se
puede realizar mediante la función on . Puede escuchar a cualquier número de nombres de mensaje.
También se pueden pasar parámetros al mensaje, como el nombre del autor y el contenido del mensaje
recibido. Una vez que el cliente recibe un mensaje, se crea un elemento div con el nombre del autor y el
contenido del mensaje en su atributo innerHTML . Se agrega al elemento div principal que muestra los
mensajes.
3. Ahora que el cliente puede recibir mensajes, debe configurarlo para poder enviarlos. Agregue el código
resaltado al archivo src/index.ts:
import "./css/main.css";
import * as signalR from "@aspnet/signalr";

const divMessages: HTMLDivElement = document.querySelector("#divMessages");


const tbMessage: HTMLInputElement = document.querySelector("#tbMessage");
const btnSend: HTMLButtonElement = document.querySelector("#btnSend");
const username = new Date().getTime();

const connection = new signalR.HubConnectionBuilder()


.withUrl("/hub")
.build();

connection.start().catch(err => document.write(err));

connection.on("messageReceived", (username: string, message: string) => {


let messageContainer = document.createElement("div");

messageContainer.innerHTML =
`<div class="message-author">${username}</div><div>${message}</div>`;

divMessages.appendChild(messageContainer);
divMessages.scrollTop = divMessages.scrollHeight;
});

tbMessage.addEventListener("keyup", (e: KeyboardEvent) => {


if (e.keyCode === 13) {
send();
}
});

btnSend.addEventListener("click", send);

function send() {
connection.send("newMessage", username, tbMessage.value)
.then(() => tbMessage.value = "");
}

El envío de mensajes a través de la conexión de WebSockets requiere llamar al método send . El primer
parámetro del método es el nombre del mensaje. Los datos del mensaje se encuentran en los otros
parámetros. En este ejemplo, se envía al servidor un mensaje identificado como newMessage . El mensaje
está formado por el nombre de usuario y la entrada del usuario desde un cuadro de texto. Si el envío
funciona, se borra el valor del cuadro de texto.
4. Agregue el método resaltado a la clase ChatHub :

using Microsoft.AspNetCore.SignalR;
using System.Threading.Tasks;

namespace SignalRWebPack.Hubs
{
public class ChatHub : Hub
{
public async Task NewMessage(string username, string message)
{
await Clients.All.SendAsync("messageReceived", username, message);
}
}
}

El código anterior difunde los mensajes recibidos a todos los usuarios conectados, una vez que el servidor
los recibe. No es necesario tener un método on genérico para recibir todos los mensajes. Basta con un
método que tenga el nombre del mensaje.
En este ejemplo, el cliente de TypeScript envía un mensaje que se identifica como newMessage . El método
NewMessage de C# espera los datos enviados por el cliente. Se realiza una llamada al método SendAsync de
Clients.All. Los mensajes recibidos se envían a todos los clientes conectados al concentrador.

Prueba de la aplicación
Confirme que la aplicación funciona con los pasos siguientes.
Visual Studio
CLI de .NET Core
1. Ejecute Webpack en modo release. Desde la ventana Consola del Administrador de paquetes, ejecute el
comando siguiente en la raíz del proyecto:

npm run release

Este comando da como resultado la entrega de los activos del lado cliente cuando se ejecuta la aplicación.
Los recursos se colocan en la carpeta wwwroot.
Webpack ha completado las tareas siguientes:
Purgar el contenido del directorio wwwroot.
Convertir TypeScript en JavaScript, proceso conocido como transpilación.
Alterar el código JavaScript generado para reducir el tamaño del archivo, proceso conocido como
minificación.
Copiar los archivos JavaScript, CSS y HTML procesados desde src en el directorio wwwroot.
Insertar los elementos siguientes en el archivo wwwroot/index.html:
Etiqueta <link> , que hace referencia al archivo wwwroot/main.<hash>.css. Esta etiqueta se
coloca inmediatamente antes de la etiqueta </head> de cierre.
Etiqueta <script> , que hace referencia al archivo wwwroot/main.<hash>.js minificado. Esta
etiqueta se coloca inmediatamente antes de la etiqueta </body> de cierre.
2. Seleccione Depurar > Iniciar sin depurar para iniciar la aplicación en un explorador sin adjuntar el
depurador. El archivo wwwroot/index.html se entrega en http://localhost:<port_number> .
3. Abra otra instancia del explorador (sirve cualquiera). Pegue la dirección URL en la barra de direcciones.
4. Elija un explorador, escriba algo en el cuadro de texto Mensaje y haga clic en el botón Enviar. El nombre
de usuario único y el mensaje se muestran en las dos páginas al instante.
Recursos adicionales
Cliente ASP.NET Core SignalR JavaScript
Usar concentradores en ASP.NET Core SignalR
Configuración de ASP.NET Core SignalR
20/09/2018 • 19 minutes to read • Edit Online

Opciones de serialización de JSON/MessagePack


ASP.NET Core SignalR admite dos protocolos para la codificación de mensajes: JSON y MessagePack. Cada
protocolo tiene opciones de configuración de serialización.
Serialización de JSON se puede configurar en el servidor mediante el AddJsonProtocol método de extensión, que
se puede agregar después AddSignalR en su Startup.ConfigureServices método. El AddJsonProtocol método
toma un delegado que recibe un options objeto. El PayloadSerializerSettings propiedad en ese objeto es un
JSON.NET JsonSerializerSettings objeto que puede usarse para configurar la serialización de argumentos y
valores devueltos. Consulte la documentación JSON.NET para obtener más detalles.
Por ejemplo, para configurar el serializador para usar nombres de propiedad "PascalCase", en lugar de los
nombres predeterminados "camelCase", use el código siguiente:

services.AddSignalR()
.AddJsonProtocol(options => {
options.PayloadSerializerSettings.ContractResolver =
new DefaultContractResolver();
});

En el cliente. NET, la misma AddJsonProtocol existe en el método de extensión HubConnectionBuilder. El


Microsoft.Extensions.DependencyInjection se debe importar el espacio de nombres para resolver el método de
extensión:

// At the top of the file:


using Microsoft.Extensions.DependencyInjection;

// When constructing your connection:


var connection = new HubConnectionBuilder()
.AddJsonProtocol(options => {
options.PayloadSerializerSettings.ContractResolver =
new DefaultContractResolver();
})
.Build();

NOTE
No es posible configurar la serialización de JSON en el cliente de JavaScript en este momento.

Opciones de serialización MessagePack


MessagePack serialización se puede configurar si se proporciona un delegado para el AddMessagePackProtocol
llamar. Consulte MessagePack en SignalR para obtener más detalles.

NOTE
No es posible configurar la serialización de MessagePack en el cliente de JavaScript en este momento.
Configurar opciones de servidor
En la tabla siguiente se describe las opciones para configurar los concentradores de SignalR:

OPCIÓN VALOR PREDETERMINADO DESCRIPCIÓN

HandshakeTimeout 15 segundos Si el cliente no envía un mensaje de


protocolo de enlace inicial dentro de
este intervalo de tiempo, se cierra la
conexión. Se trata de una opción
avanzada que sólo debería modificarse
si se producen errores de tiempo de
espera del protocolo de enlace debido a
la latencia de red graves. Para obtener
más detalles sobre el proceso de
negociación, consulte el especificación
del protocolo SignalR Hub.

KeepAliveInterval 15 segundos Si el servidor no ha enviado un mensaje


dentro de este intervalo, se envía
automáticamente un mensaje ping para
mantener abierta la conexión. Al
cambiar KeepAliveInterval , cambie
el ServerTimeout /
serverTimeoutInMilliseconds
configuración del cliente. Recomendado
ServerTimeout /
serverTimeoutInMilliseconds valor
es doble el KeepAliveInterval valor.

SupportedProtocols Todos los protocolos instalados Protocolos admitidos por este


concentrador. De forma
predeterminada, se permiten todos los
protocolos registrados en el servidor,
pero se pueden quitar protocolos de
esta lista para deshabilitar los
protocolos específicos para los
concentradores individuales.

EnableDetailedErrors false Si true detallados se devuelven los


mensajes de excepción a los clientes
cuando se produce una excepción en un
método de concentrador. El valor
predeterminado es false , ya que
estos mensajes de excepción pueden
contener información confidencial.

Se pueden configurar opciones para todos los centros proporcionando un delegado de opciones para la
AddSignalR llamar Startup.ConfigureServices .

public void ConfigureServices(IServiceCollection services)


{
services.AddSignalR(hubOptions =>
{
hubOptions.EnableDetailedErrors = true;
hubOptions.KeepAliveInterval = TimeSpan.FromMinutes(1);
});
}

Opciones para un único centro de invalidan las opciones globales de AddSignalR y pueden configurarse mediante
AddHubOptions<T >:

services.AddSignalR().AddHubOptions<MyHub>(options =>
{
options.EnableDetailedErrors = true;
});

Use HttpConnectionDispatcherOptions para configurar opciones avanzadas relacionadas con transportes y


administración de búfer de memoria. Estas opciones se configuran pasando un delegado para MapHub<T >.

OPCIÓN VALOR PREDETERMINADO DESCRIPCIÓN

ApplicationMaxBufferSize 32 KB El número máximo de bytes recibidos


del cliente que los búferes del servidor.
Al aumentar este valor permite que el
servidor recibir los mensajes más
grandes, pero puede repercutir
negativamente en el consumo de
memoria.

AuthorizationData Recopilar automáticamente los datos de Una lista de IAuthorizeData los objetos
la Authorize atributos aplicados a la utilizados para determinar si un cliente
clase de concentrador. está autorizado para conectarse al
concentrador.

TransportMaxBufferSize 32 KB El número máximo de bytes enviados


por la aplicación que los búferes del
servidor. Al aumentar este valor permite
que el servidor enviar los mensajes más
grandes, pero puede repercutir
negativamente en el consumo de
memoria.

Transports Se habilitan todos los transportes. Una máscara de bits de


HttpTransportType valores que
pueden restringir los transportes que
un cliente puede usar para conectarse.

LongPolling Véalo a continuación. Opciones adicionales específicas para el


transporte de sondeo largo.

WebSockets Véalo a continuación. Opciones adicionales específicas para el


transporte de WebSockets.

El transporte de sondeo largo tiene opciones adicionales que se pueden configurar mediante el LongPolling
propiedad:

OPCIÓN VALOR PREDETERMINADO DESCRIPCIÓN

PollTimeout 90 segundos La cantidad máxima de tiempo que el


servidor espera para que un mensaje
para enviárselo al cliente antes de
finalizar una solicitud de sondeo única.
Al disminuir este valor hace que el
cliente emitir solicitudes de sondeo de
nuevo con más frecuencia.

El transporte de WebSocket tiene opciones adicionales que se pueden configurar mediante el WebSockets
propiedad:

OPCIÓN VALOR PREDETERMINADO DESCRIPCIÓN

CloseTimeout 5 segundos Después de cerrar el servidor, si se


produce un error en el cliente cerrar
dentro de este intervalo de tiempo, se
termina la conexión.

SubProtocolSelector null Un delegado que puede usarse para


establecer el Sec-WebSocket-Protocol
encabezado en un valor personalizado.
El delegado recibe los valores solicitados
por el cliente como entrada y se espera
que devuelva el valor deseado.

Configurar las opciones de cliente


Se pueden configurar las opciones de cliente en el HubConnectionBuilder tipo (disponible en los clientes de .NET y
JavaScript), así como en el HubConnection propio.
Configurar el registro
El registro está configurado en el cliente de .NET mediante el ConfigureLogging método. Registro de proveedores
y los filtros se puede registrar en la misma manera, tal como están en el servidor. Consulte la registro en ASP.NET
Core documentación para obtener más información.

NOTE
Con el fin de registrar los proveedores de registro, debe instalar los paquetes necesarios. Consulte la proveedores de registro
integrados sección de la documentación para obtener una lista completa.

Por ejemplo, para habilitar el registro de consola, instale el Microsoft.Extensions.Logging.Console paquete NuGet.
Llame a la AddConsole método de extensión:

var connection = new HubConnectionBuilder()


.WithUrl("https://example.com/myhub")
.ConfigureLogging(logging => {
logging.SetMinimumLevel(LogLevel.Information);
logging.AddConsole();
})
.Build();

En el cliente de JavaScript, un proceso similar configureLogging método existe. Proporcione un LogLevel valor
que indica el nivel mínimo de los mensajes de registro para generar. Los registros se escriben en la ventana de
consola del explorador.

let connection = new signalR.HubConnectionBuilder()


.withUrl("/myhub")
.configureLogging(signalR.LogLevel.Information)
.build();

NOTE
Para deshabilitar el registro por completo, especifique signalR.LogLevel.None en el configureLogging método.
A continuación, se muestran los niveles de registro disponibles para el cliente de JavaScript. Establecer el nivel de
registro en uno de estos valores habilita el registro de mensajes en o superior ese nivel.

NIVEL DESCRIPCIÓN

None Se registra ningún mensaje.

Critical Mensajes que indican un error en toda la aplicación.

Error Mensajes que indican un error en la operación actual.

Warning Mensajes que indican un problema grave.

Information Mensajes informativos.

Debug Mensajes de diagnóstico útiles para depurar.

Trace Mensajes de diagnóstico muy detallados diseñados para


diagnosticar problemas específicos.

Configurar transportes permitidos


Los transportes utilizados por SignalR se pueden configurar en el WithUrl llamar ( withUrl en JavaScript). Una
operación OR bit a bit de los valores de HttpTransportType puede usarse para restringir el cliente para que solo
use los transportes especificados. Todos los transportes están habilitados de forma predeterminada.
Por ejemplo, para deshabilitar el transporte de los eventos, pero permitir conexiones WebSockets y Long Polling:

var connection = new HubConnectionBuilder()


.WithUrl("https://example.com/myhub", HttpTransportType.WebSockets | HttpTransportType.LongPolling)
.Build();

En el cliente de JavaScript, se configuran los transportes estableciendo el transport campo en el objeto de


opciones proporcionado para withUrl :

let connection = new signalR.HubConnectionBuilder()


.withUrl("/myhub", { transport: signalR.HttpTransportType.WebSockets |
signalR.HttpTransportType.LongPolling })
.build();

Configurar la autenticación de portador


Para proporcionar datos de autenticación junto con las solicitudes de SignalR, use el AccessTokenProvider opción (
accessTokenFactory en JavaScript) para especificar una función que devuelve el token de acceso deseado. En el
cliente. NET, este token de acceso se pasa como un HTTP "Autenticación del portador" token (mediante el
Authorization encabezado con un tipo de Bearer ). En el cliente de JavaScript, se usa el token de acceso como un
token de portador, excepto en algunos casos donde las API de explorador restringir la capacidad de aplicar los
encabezados (específicamente, en las solicitudes de los eventos y WebSockets). En estos casos, el token de acceso
se proporciona como un valor de cadena de consulta access_token .
En el cliente. NET, el AccessTokenProvider opción puede especificarse utilizando el delegado de opciones en
WithUrl :
var connection = new HubConnectionBuilder()
.WithUrl("https://example.com/myhub", options => {
options.AccessTokenProvider = async () => {
// Get and return the access token.
};
})
.Build();

En el cliente de JavaScript, el token de acceso se configura estableciendo el accessTokenFactory campo en el objeto


de opciones de withUrl :

let connection = new signalR.HubConnectionBuilder()


.withUrl("/myhub", {
accessTokenFactory: () => {
// Get and return the access token.
// This function can return a JavaScript Promise if asynchronous
// logic is required to retrieve the access token.
}
})
.build();

Configurar el tiempo de espera y las opciones de mantenimiento


Opciones adicionales para configurar el tiempo de espera y el comportamiento de mantenimiento están
disponibles en el HubConnection propio objeto:

OPCIÓN DE .NET OPCIÓN DE JAVASCRIPT VALOR PREDETERMINADO DESCRIPCIÓN

ServerTimeout serverTimeoutInMilliseconds 30 segundos (30.000 Tiempo de espera para la


milisegundos) actividad del servidor. Si el
servidor no ha enviado un
mensaje en este intervalo, el
cliente considera que las ha
desconectado el servidor y
los desencadenadores la
Closed eventos ( onclose
en JavaScript). Este valor
debe ser lo suficientemente
grande como para un
mensaje ping se envíe desde
el servidor y recibidos por el
cliente dentro del intervalo
de tiempo de espera. El valor
recomendado es de un
número al menos el doble
del servidor
KeepAliveInterval valor,
para dejar tiempo para pings
de llegada.
OPCIÓN DE .NET OPCIÓN DE JAVASCRIPT VALOR PREDETERMINADO DESCRIPCIÓN

HandshakeTimeout No se puede configurar 15 segundos Tiempo de espera para el


protocolo de enlace de
servidor inicial. Si el servidor
no envía una respuesta de
protocolo de enlace en este
intervalo, el cliente cancela el
protocolo de enlace y los
desencadenadores la
Closed eventos ( onclose
en JavaScript). Se trata de
una opción avanzada que
sólo debería modificarse si se
producen errores de tiempo
de espera del protocolo de
enlace debido a la latencia
de red graves. Para obtener
más detalles sobre el
proceso de negociación,
consulte el especificación del
protocolo SignalR Hub.

En el cliente. NET, se especifican los valores de tiempo de espera como TimeSpan valores. En el cliente de
JavaScript, los valores de tiempo de espera se especifican como un número que indica la duración en
milisegundos.
Configurar opciones adicionales
Se pueden configurar opciones adicionales en el WithUrl ( withUrl en JavaScript) método HubConnectionBuilder :

OPCIÓN DE .NET OPCIÓN DE JAVASCRIPT VALOR PREDETERMINADO DESCRIPCIÓN

AccessTokenProvider accessTokenFactory null Una función que devuelve


una cadena que se
proporciona como un token
de autenticación de
portador en solicitudes
HTTP.

SkipNegotiation skipNegotiation false Establezca esta opción en


true para omitir el paso de
negociación. Solo se
admite cuando el
transporte de WebSockets
es el único tipo de
transporte habilitado. No
se puede habilitar esta
configuración al usar Azure
SignalR Service.

ClientCertificates No se puede configurar * Empty Una colección de certificados


TLS que se envían autenticar
las solicitudes.

Cookies No se puede configurar * Empty Una colección de cookies


HTTP para enviar con cada
solicitud HTTP.
OPCIÓN DE .NET OPCIÓN DE JAVASCRIPT VALOR PREDETERMINADO DESCRIPCIÓN

Credentials No se puede configurar * Empty Credenciales que se enviará


con todas las solicitudes
HTTP.

CloseTimeout No se puede configurar * 5 segundos Solo WebSockets. La


cantidad máxima de tiempo
de espera a que el cliente
después del cierre del
servidor confirmar la
solicitud de cierre. Si el
servidor no reconoció el
cierre dentro de este tiempo,
el cliente se desconecta.

Headers No se puede configurar * Empty Un diccionario de


encabezados HTTP
adicionales que se enviará
con todas las solicitudes
HTTP.

HttpMessageHandlerFactory No se puede configurar * null Un delegado que puede


usarse para configurar o
reemplazar el
HttpMessageHandler
utilizado para enviar
solicitudes HTTP. No se
utiliza para las conexiones de
WebSocket. Este delegado
debe devolver un valor
distinto de null, y recibe el
valor predeterminado como
un parámetro. Modificar la
configuración en ese valor
predeterminado y devolverlo
o devolver un nuevo
HttpMessageHandler
instancia. Cuando
Asegúrese de reemplazar
el controlador copiar la
configuración que desee
impedir que el
controlador
proporcionado, en caso
contrario, las opciones
configuradas (por
ejemplo, Cookies y
encabezados) no se
aplicarán al nuevo
controlador.

Proxy No se puede configurar * null Un proxy HTTP que se


utilizará al enviar solicitudes
HTTP.
OPCIÓN DE .NET OPCIÓN DE JAVASCRIPT VALOR PREDETERMINADO DESCRIPCIÓN

UseDefaultCredentials No se puede configurar * false Establezca este valor


booleano para enviar las
credenciales
predeterminadas para las
solicitudes HTTP y
WebSockets. Esto permite el
uso de autenticación de
Windows.

WebSocketConfiguration No se puede configurar * null Un delegado que puede


usarse para configurar
opciones adicionales de
WebSocket. Recibe una
instancia de
ClientWebSocketOptions
que puede utilizarse para
configurar las opciones.

Opciones marcadas con un asterisco (*) no son configurables en el cliente de JavaScript, debido a limitaciones en
el Explorador de API.
En el cliente. NET, estas opciones se pueden modificar por el delegado de las opciones proporcionado para
WithUrl :

var connection = new HubConnectionBuilder()


.WithUrl("https://example.com/myhub", options => {
options.Headers["Foo"] = "Bar";
options.Cookies.Add(new Cookie(/* ... */);
options.ClientCertificates.Add(/* ... */);
})
.Build();

En el cliente de JavaScript, estas opciones se pueden proporcionar en un objeto de JavaScript proporcionado a


withUrl :

let connection = new signalR.HubConnectionBuilder()


.withUrl("/myhub", {
skipNegotiation: true,
transport: signalR.HttpTransportType.WebSockets
})
.build();

Recursos adicionales
Tutorial: Introducción a SignalR en ASP.NET Core
Usar concentradores en ASP.NET Core SignalR
Cliente ASP.NET Core SignalR JavaScript
Cliente de .NET de ASP.NET Core SignalR
Usar el protocolo de MessagePack concentrador de SignalR para ASP.NET Core
Plataformas compatibles con ASP.NET Core SignalR
Autenticación y autorización en ASP.NET Core
SignalR
25/09/2018 • 8 minutes to read • Edit Online

Por Andrew Stanton-Nurse


Ver o descargar el código de ejemplo (cómo descargar)

Autenticar a los usuarios conectarse a un concentrador SignalR


Se puede usar SignalR con autenticación de ASP.NET Core para asociar un usuario a cada conexión. En, un centro
de datos de autenticación se pueden acceder desde el HubConnectionContext.User propiedad. La autenticación
permite que el concentrador llamar a métodos en todas las conexiones asociadas a un usuario (consulte
administrar usuarios y grupos en SignalR para obtener más información). Varias conexiones pueden asociarse con
un solo usuario.
Autenticación con cookies
Autenticación con cookies en una aplicación basada en explorador, permite que sus credenciales de usuario
existente fluya automáticamente a las conexiones de SignalR. Cuando se usa el explorador del cliente, no se
necesita ninguna configuración adicional. Si el usuario inicia sesión en su aplicación, la conexión de SignalR hereda
automáticamente esta autenticación.
Autenticación con cookies no se recomienda a menos que la aplicación solo necesita autenticar a los usuarios
desde el explorador del cliente. Cuando se usa el cliente.NET, el Cookies propiedad se puede configurar en el
.WithUrl llamada con el fin de proporcionar una cookie. Sin embargo, requiere la aplicación para proporcionar
una API para intercambiar datos de autenticación para una cookie mediante la autenticación de cookies del cliente.
NET.
Autenticación de token de portador
Autenticación de token de portador es el enfoque recomendado al usar a los clientes que no sea el explorador del
cliente. En este enfoque, el cliente proporciona un token de acceso que el servidor valida y se utiliza para identificar
al usuario. Los detalles de autenticación de token de portador son más allá del ámbito de este documento. En el
servidor, la autenticación de token de portador se configura mediante el middleware de portador JWT.
En el cliente de JavaScript, se puede proporcionar el token mediante el accessTokenFactory opción.

this.connection = new signalR.HubConnectionBuilder()


.withUrl("/hubs/chat", { accessTokenFactory: () => this.loginToken })
.build();

En el cliente. NET, hay un proceso similar AccessTokenProvider propiedad que se puede usar para configurar el
token:

var connection = new HubConnectionBuilder()


.WithUrl("https://example.com/myhub", options =>
{
options.AccessTokenProvider = () => Task.FromResult(_myAccessToken);
})
.Build();
NOTE
Se llama a la función de token de acceso que proporcione antes cada solicitud HTTP de SignalR. Si necesita renovar el token
con el fin de mantener la conexión activa (porque lo puede expirar durante la conexión), hacerlo desde esta función y
devolver el token actualizado.

En las API web estándar, se envían los tokens de portador en un encabezado HTTP. Sin embargo, SignalR es no se
puede establecer estos encabezados en los exploradores cuando se usa algunos transportes. Cuando se usa
WebSockets y los eventos, el token se transmite como un parámetro de cadena de consulta. Para admitir esto en el
servidor, se requiere configuración adicional:

public void ConfigureServices(IServiceCollection services)


{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();

services.AddAuthentication(options =>
{
// Identity made Cookie authentication the default.
// However, we want JWT Bearer Auth to be the default.
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
// Configure JWT Bearer Auth to expect our security key
options.TokenValidationParameters =
new TokenValidationParameters
{
LifetimeValidator = (before, expires, token, param) =>
{
return expires > DateTime.UtcNow;
},
ValidateAudience = false,
ValidateIssuer = false,
ValidateActor = false,
ValidateLifetime = true,
IssuerSigningKey = SecurityKey
};

// We have to hook the OnMessageReceived event in order to


// allow the JWT authentication handler to read the access
// token from the query string when a WebSocket or
// Server-Sent Events request comes in.
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];

// If the request is for our hub...


var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) &&
(path.StartsWithSegments("/hubs/chat")))
{
// Read the token out of the query string
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
};
});

services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

services.AddSignalR();

// Change to use Name as the user identifier for SignalR


// WARNING: This requires that the source of your JWT token
// ensures that the Name claim is unique!
// If the Name claim isn't unique, users could receive messages
// intended for a different user!
services.AddSingleton<IUserIdProvider, NameUserIdProvider>();
}

Autenticación de Windows
Si autenticación de Windows está configurado en la aplicación, SignalR puede usar esa identidad para proteger los
concentradores. Sin embargo, para enviar mensajes a usuarios individuales, deberá agregar un proveedor de Id. de
usuario personalizado. Esto es porque el sistema de autenticación de Windows no proporciona la notificación
"NameIdentifier" que usa SignalR para determinar el nombre de usuario.
Agregue una nueva clase que implementa IUserIdProvider y recuperar una de las notificaciones del usuario que
se usará como el identificador. Por ejemplo, para usar la notificación "Name" (que es el nombre de usuario de
Windows en el formulario [Domain]\[Username] ), cree la siguiente clase:

public class NameUserIdProvider : IUserIdProvider


{
public string GetUserId(HubConnectionContext connection)
{
return connection.User?.FindFirst(ClaimTypes.Name)?.Value;
}
}

En lugar de ClaimTypes.Name , puede usar cualquier valor de la User (por ejemplo, el identificador SID de
Windows, etcetera.).

NOTE
El valor que elija debe ser único entre todos los usuarios en el sistema. En caso contrario, un mensaje destinado a un usuario
podría acabar va a un usuario diferente.

Registrar este componente en su Startup.ConfigureServices método después la llamada a .AddSignalR

public void ConfigureServices(IServiceCollection services)


{
// ... other services ...

services.AddSignalR();
services.AddSingleton<IUserIdProvider, NameUserIdProvider>();
}

En el cliente. NET, la autenticación de Windows debe estar habilitada estableciendo el UseDefaultCredentials


propiedad:
var connection = new HubConnectionBuilder()
.WithUrl("https://example.com/myhub", options =>
{
options.UseDefaultCredentials = true;
})
.Build();

Autenticación de Windows solo es compatible con el explorador del cliente cuando se usa Microsoft Internet
Explorer o Microsoft Edge.

Autorizar a los usuarios acceso a concentradores y métodos de


concentrador
De forma predeterminada, se pueden llamar a todos los métodos en un centro de un usuario autenticado. Con el
fin de solicitar la autenticación, se aplican la Authorize al concentrador de atributo:

[Authorize]
public class ChatHub: Hub
{
}

Puede usar los argumentos de constructor y propiedades de la [Authorize] atributo para restringir el acceso solo
a los usuarios de coincidencia específico las directivas de autorización. Por ejemplo, si tiene una directiva de
autorización personalizada denominada MyAuthorizationPolicy puede asegurarse de que solo los usuarios que
coinciden con dicha directiva pueden tener acceso a centro con el código siguiente:

[Authorize("MyAuthorizationPolicy")]
public class ChatHub: Hub
{
}

Los métodos de concentrador individuales pueden tener el [Authorize] también aplicado el atributo. Si el usuario
actual no coincide con la directiva se aplica al método, se devuelve un error al llamador:

[Authorize]
public class ChatHub: Hub
{
public async Task Send(string message)
{
// ... send a message to all users ...
}

[Authorize("Administrators")]
public void BanUser(string userName)
{
// ... ban a user from the chat room (something only Administrators can do) ...
}
}
Consideraciones de seguridad en ASP.NET Core
SignalR
18/07/2018 • 6 minutes to read • Edit Online

Por Andrew Stanton-Nurse

Información general
SignalR proporciona una serie de protecciones de seguridad de forma predeterminada. Es importante entender
cómo configurar estas protecciones.
Uso compartido de recursos entre orígenes
Los recursos entre orígenes (CORS ) de uso compartido puede utilizarse para permitir que las conexiones de
origen cruzado SignalR en el explorador. Si el código de JavaScript está hospedado en otro nombre de dominio de
la aplicación de SignalR, tendrá que habilitar el middleware de ASP.NET Core CORS con el fin de permitir la
conexión. En general, permitir solicitudes entre orígenes solo desde los dominios que controla. Por ejemplo, si su
sitio está hospedado en http://www.example.com y la aplicación de SignalR se hospeda en
http://signalr.example.com , debe configurar CORS en la aplicación de SignalR para permitir únicamente el origen
www.example.com .

Para obtener más información sobre cómo configurar la CORS, vea la documentación sobre ASP.NET Core CORS.
SignalR requiere las siguientes directivas CORS para poder funcionar correctamente:
La directiva debe permitir los orígenes específicos previsto o permitir que cualquier origen (no recomendado).
Métodos HTTP GET y POST debe estar permitido.
Deben habilitarse las credenciales, incluso cuando no está usando la autenticación.
Por ejemplo, la siguiente directiva CORS permite hospedado en un cliente del explorador SignalR
http://example.com para tener acceso a la aplicación de SignalR:

public void Configure(IApplicationBuilder app)


{
// ... other middleware ...

// Make sure the CORS middleware is ahead of SignalR.


app.UseCors(builder => {
builder.WithOrigins("http://example.com")
.AllowAnyHeader()
.WithMethods("GET", "POST")
.AllowCredentials();
});

// ... other middleware ...

app.UseSignalR();

// ... other middleware ...


}

NOTE
SignalR no es compatible con la característica CORS integrada en Azure App Service.
Registro de token de acceso
Al usar WebSockets o los eventos, el explorador del cliente envía el token de acceso en la cadena de consulta. Esto
es generalmente tan seguro como usar el estándar Authorization encabezado, sin embargo, la dirección URL para
cada solicitud de registro de muchos servidores web, incluida la cadena de consulta. Esto significa que el token de
acceso que puede incluirse en los registros. Considere la posibilidad de revisar la configuración de registro del
servidor web para evitar esta información de registro.
Excepciones
Los mensajes de excepción normalmente se consideran información confidencial que no debe mostrarse a un
cliente. De forma predeterminada, SignalR no envía los detalles de una excepción producida por un método de
concentrador al cliente. En su lugar, el cliente recibe un mensaje genérico que indica que un error.Puede invalidar
este comportamiento estableciendo el EnableDetailedErrors configuración.
Administración de búfer
SignalR usa búferes por conexión con el fin de administrar los mensajes entrantes y salientes. De forma
predeterminada, SignalR limita estos búferes a 32KB. Esto significa que el mensaje posible más grande que puede
enviar un cliente o servidor es 32KB. Esto también significa que la cantidad máxima de memoria consumida por
una conexión para los mensajes es 32KB. Si conoce que los mensajes siempre son menores que este límite, puede
reducir este tamaño para impedir que un cliente puede enviar un mensaje mayor y forzar al servidor para asignar
memoria para aceptarla. De forma similar, si sabe que los mensajes son mayores que este límite, se puede
aumentar. Sin embargo, tenga en cuenta que al aumentar este límite significa que el cliente es capaz de hacer que
el servidor asignar memoria adicional y puede reducir el número de conexiones simultáneas que puede controlar
la aplicación.
Hay límites independientes para mensajes entrantes y salientes, ambos se pueden configurar en el
HttpConnectionDispatcherOptions configurado en el objeto MapHub :

ApplicationMaxBufferSize representa el número máximo de bytes desde el cliente que los búferes del servidor.
Si el cliente intenta enviar un mensaje supere ese límite, se puede cerrar la conexión.
TransportMaxBufferSize representa el número máximo de bytes que el servidor puede enviar. Si el servidor
intenta enviar un mensaje (incluye los valores devueltos de métodos de concentrador) supera este límite, se
producirá una excepción.
Establecer el límite en 0 deshabilita totalmente el límite. Sin embargo, esto debe hacerse con sumo cuidado.
Quitar el límite permite que un cliente enviar un mensaje de cualquier tamaño. Esto se puede utilizar un cliente
malintencionado para provocar un exceso de memoria que se asignen, lo que podría disminuir considerablemente
el número de conexiones simultáneas que puede admitir la aplicación.
Usar el protocolo de MessagePack concentrador de
SignalR para ASP.NET Core
09/08/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 comenzar.

¿Qué es MessagePack?
MessagePack es un formato de serialización binaria es rápido y compacto. Es útil cuando constituyen un
problema de rendimiento y ancho de banda porque crea mensajes más pequeños en comparación con JSON.
Dado que es un formato binario, los mensajes son ilegibles al examinar los registros y seguimientos de red a
menos que los bytes se pasan a través de un analizador MessagePack. Tiene compatibilidad integrada con el
formato MessagePack SignalR y proporciona las API para el cliente y servidor usar.

Configurar MessagePack en el servidor


Para habilitar el protocolo de Hub MessagePack en el servidor, instale el
Microsoft.AspNetCore.SignalR.Protocols.MessagePack paquete en la aplicación. En el archivo Startup.cs agregue
AddMessagePackProtocol a la AddSignalR llamada a habilitar la compatibilidad con MessagePack en el servidor.

NOTE
JSON está habilitado de forma predeterminada. Agregar MessagePack habilita la compatibilidad con clientes MessagePack y
JSON.

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 usarse para configurar las
opciones de serialización MessagePack. Para obtener más información sobre cómo funcionan las resoluciones,
visite la biblioteca MessagePack en MessagePack-CSharp. Atributos se pueden usar 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
paquetes y llame a AddMessagePackProtocol en HubConnectionBuilder .

var hubConnection = new HubConnectionBuilder()


.WithUrl("/chatHub")
.AddMessagePackProtocol()
.Build();

NOTE
Esto AddMessagePackProtocol llamada toma un delegado para configurar las opciones al igual que el servidor.

Cliente de JavaScript
MessagePack compatibilidad con el cliente de Javascript proporciona el @aspnet/signalr-protocol-msgpack
paquete NPM.

npm install @aspnet/signalr-protocol-msgpack

Después de instalar el paquete de npm, el módulo se puede utilizar directamente a través de un cargador de
módulos de JavaScript o importado en el explorador haciendo referencia a la node_modules\@aspnet\signalr-
protocol-msgpack\dist\browser\signalr-protocol-msgpack.js archivo. En un explorador el msgpack5 también se
debe hacer referencia a la biblioteca. Use un <script> etiqueta para crear una referencia. La biblioteca puede
encontrarse en node_modules\msgpack5\dist\msgpack5.js.

NOTE
Cuando se usa el <script> elemento, el orden es importante. Si signalr-protocol-msgpack.js se hace referencia antes de
msgpack5.js, se produce un error al intentar conectarse con MessagePack. signalr.js también es necesaria antes de signalr-
protocol-msgpack.js.

<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
Usar la transmisión por secuencias en ASP.NET Core
SignalR
18/07/2018 • 4 minutes to read • Edit Online

Por Brennan Conroy


ASP.NET Core SignalR es compatible con la transmisión por secuencias los valores devueltos de métodos de
servidor. Esto es útil para escenarios de fragmentos de datos de procedencia con el tiempo. Cuando se transmite
un valor devuelto al cliente, significará que se ha enviado cada fragmento al cliente en cuanto se convierte en
disponible, en lugar de tener que esperar todos los datos estén disponibles.
Vea o descargue el código de ejemplo (cómo descargarlo)

Configurar el centro
Un método de concentrador se convierte automáticamente en un método de concentrador de transmisión por
secuencias cuando vuelve una ChannelReader<T> o Task<ChannelReader<T>> . A continuación es un ejemplo que
muestra los conceptos básicos de transmisión de datos al cliente. Cada vez que se escribe un objeto en el
ChannelReader ese objeto inmediatamente se envía al cliente. Al final, el ChannelReader completada para indicar al
cliente la secuencia está cerrada.

NOTE
Escribir en el ChannelReader en un subproceso en segundo plano y vuelva el ChannelReader tan pronto como sea
posible. Las demás invocaciones de 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.
Pase 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 la
secuencia y representa el flujo en el cliente. Para leer datos, un patrón común es para recorrer en bucle
WaitToReadAsync y llamar a TryRead cuando los datos están disponibles. El bucle finalizará cuando el servidor ha
cerrado la secuencia o el token de cancelación pasado 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, que es el nombre del método de concentrador
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 devoluciones de llamada para recibir notificaciones, 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
Diferencias entre SignalR de ASP.NET y ASP.NET Core
SignalR
11/09/2018 • 6 minutes to read • Edit Online

ASP.NET Core SignalR no es compatible con clientes o servidores para ASP.NET SignalR. En este artículo se
detalla las características que se han quitado o cambiado en ASP.NET Core SignalR.

Cómo identificar la versión de SignalR


ASP.NET SIGNALR SIGNALR DE ASP.NET CORE

Paquete de NuGet de servidor Microsoft.AspNet.SignalR Microsoft.AspNetCore.App (.NET Core)


Microsoft.AspNetCore.SignalR (.NET
Framework)

Paquetes NuGet del cliente Microsoft.AspNet.SignalR.Client Microsoft.AspNetCore.SignalR.Client


Microsoft.AspNet.SignalR.JS

Paquete de npm de cliente signalr @aspnet/signalr

Tipo de aplicación de servidor ASP.NET (System.Web) o autohospedaje ASP.NET Core


OWIN

Plataformas de servidor compatibles .NET framework 4.5 o posterior .NET Framework 4.6.1 o versiones
posteriores
.NET core 2.1 o posterior

Diferencias de características
Reconexiones automática
Ya no se admiten las reconexiones automática. Anteriormente, SignalR intentó volver a conectarse al servidor si se
quitó la conexión. Ahora, si el cliente se desconecta el usuario debe iniciar explícitamente una nueva conexión si
desea volver a conectar.
Soporte de protocolo
ASP.NET Core SignalR es compatible con JSON, así como un nuevo protocolo binario según MessagePack.
Además, se pueden crear protocolos personalizados.

Diferencias en el servidor
Las bibliotecas de servidor de ASP.NET Core SignalR se incluyen en el Microsoft.AspNetCore.App metapaquete
paquete que forma parte de la aplicación Web ASP.NET Core plantilla de Razor y MVC proyectos.
ASP.NET Core SignalR es un middleware de ASP.NET Core, por lo que se debe configurar mediante una llamada
a AddSignalR en Startup.ConfigureServices .

services.AddSignalR()

Para configurar el enrutamiento, se asignan las rutas a los concentradores dentro de la UseSignalR llame al
método el Startup.Configure método.

app.UseSignalR(routes =>
{
routes.MapHub<ChatHub>("/hub");
});

Ahora requeridas sesiones


Debido a cómo escalar horizontalmente trabajado en SignalR de ASP.NET, los clientes podrían volver a conectarse
y enviar mensajes a cualquier servidor de la granja de servidores. Debido a cambios en el modelo de escalabilidad
horizontal, así como que no admiten reconexiones, ya no se admite. Una vez que el cliente se conecta al servidor,
debe interactuar con el mismo servidor para la duración de la conexión.
Centro único por conexión
En ASP.NET Core SignalR, se ha simplificado el modelo de conexión. Las conexiones se realizan directamente en
un único centro, en lugar de una sola conexión que se usa para compartir el acceso a varios centros.
Streaming
ASP.NET Core SignalR ahora admite datos de streaming desde el concentrador para el cliente.
Estado
Se ha quitado la capacidad de pasar información de estado arbitraria entre los clientes y el centro de (a menudo
denominada HubState), así como compatibilidad con los mensajes de progreso. En este momento no hay ningún
homólogo de los servidores proxy de concentrador.

Diferencias en el cliente
TypeScript
El cliente de ASP.NET Core SignalR está escrito en TypeScript. Puede escribir en JavaScript o TypeScript al usar el
cliente JavaScript.
El cliente de JavaScript está hospedado en npm
En versiones anteriores, el cliente de JavaScript se obtuvo a través de un paquete de NuGet en Visual Studio. Para
las versiones principales, el @aspnet/signalr paquete npm contiene las bibliotecas de JavaScript. Este paquete no
se incluye en el aplicación Web ASP.NET Core plantilla. Utilice npm para obtener e instalar el @aspnet/signalr
paquete npm.

npm init -y
npm install @aspnet/signalr

jQuery
Se quitó la dependencia de jQuery, sin embargo, los proyectos pueden seguir usando jQuery.
Sintaxis de método del cliente de JavaScript
La sintaxis de JavaScript ha cambiado desde la versión anterior de SignalR. En lugar de usar el $connection , cree
una conexión con el HubConnectionBuilder API.

const connection = new signalR.HubConnectionBuilder()


.withUrl("/hub")
.build();

Use la en método para especificar métodos de cliente que se puede llamar la central.
connection.on("ReceiveMessage", (user, message) => {
const msg = message.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
const encodedMsg = user + " says " + msg;
log(encodedMsg);
});

Después de crear el método de cliente, inicie la conexión de concentrador. Cadena de un catch método para iniciar
o controlar los errores.

connection.start().catch(err => console.error(err.toString()));

Servidores proxy de concentrador


Automáticamente ya no se generan servidores proxy de concentrador. En su lugar, el nombre del método se pasa a
la invocar API como una cadena.
.NET y otros clientes
El Microsoft.AspNetCore.SignalR.Client paquete NuGet contiene las bibliotecas de cliente .NET para ASP.NET
Core SignalR.
Use la HubConnectionBuilder para crear y generar una instancia de una conexión a un concentrador.

connection = new HubConnectionBuilder()


.WithUrl("url")
.Build();

Diferencias de escalabilidad horizontal


SignalR de ASP.NET es compatible con SQL Server y Redis. ASP.NET Core SignalR es compatible con Azure
SignalR Service y Redis.
ASP.NET
Escalabilidad horizontal de SignalR con Azure Service Bus
Escalabilidad horizontal de SignalR con Redis
Escalabilidad horizontal de SignalR con SQL Server
ASP.NET Core
Servicio Azure SignalR

Recursos adicionales
Concentradores
Cliente de JavaScript
Cliente .NET
Plataformas compatibles
Probar, depurar y solucionar problemas en ASP.NET
Core
04/07/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
Información sobre cómo depurar con Visual Studio
Descubra las características del depurador de Visual Studio en un tutorial paso a paso.
Depuración con Visual Studio Code
Descubra la compatibilidad de la depuración integrada en Visual Studio Code.
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
Explore 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
Descubra 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 en ASP.NET Core
23/08/2018 • 34 minutes to read • Edit Online

Por Halter y Steve Smith


Las 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 prueba web y un servidor de prueba en memoria.
En este tema se da por supuesto un conocimiento básico de las pruebas unitarias. Si no conoce los conceptos
de pruebas, consulte el Unit Testing en .NET Core y .NET Standard 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 se da por supuesto 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

NOTE
Para probar las SPA, se recomienda una herramienta como Selenium, que puede automatizar un explorador.

Introducción a las pruebas de integración


Las pruebas de integración para evaluar los componentes de una aplicación en un nivel más amplio que
pruebas unitarias. Las pruebas unitarias se utilizan para probar los componentes de software independiente,
como los métodos de la clase individual. Confirmación que las pruebas de integración que dos o más
componentes de aplicación funcionan conjuntamente para producir un resultado esperado, posiblemente
incluidos todos los componentes necesarios para procesar una solicitud.
Estas pruebas más amplias se utilizan para probar la infraestructura de la aplicación y un marco completo, a
menudo incluye los siguientes componentes:
Base de datos
Sistema de archivos
Dispositivos de red
Canalización de solicitud y respuesta
Pruebas unitarias use fabricado componentes, denominados fakes o objetos ficticios, en lugar de los
componentes de infraestructura.
A diferencia de las 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.
Tardan más en ejecutarse.
Por lo tanto, limite el uso de las pruebas de integración para los escenarios de infraestructura más importantes.
Si un comportamiento se puede probar con una prueba unitaria o una prueba de integración, elija la prueba
unitaria.

TIP
No escriba las pruebas de integración para todas las posibles permutaciones de acceso de archivos y datos con las bases
de datos y sistemas de archivos. Independientemente de cuántos lugares a través de una aplicación interactuar con las
bases de datos y sistemas de archivos, un conjunto con foco de lectura, escritura, actualización y eliminación integración
pruebas son normalmente capaces de adecuadamente las pruebas de la base de datos y los componentes del sistema de
archivos. Use pruebas unitarias para las pruebas de rutina de la lógica del método que interactúan estos componentes.
En las pruebas unitarias, el uso de la infraestructura de fakes/simulacros resultado en ejecución de pruebas más rápida.

NOTE
En las conversaciones de pruebas de integración, se suele denominar el proyecto probado el sistema sometido a prueba,
o "SUT" para abreviar.

Pruebas de integración de ASP.NET Core


Las pruebas de integración en 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 del servidor de prueba
para controlar las solicitudes y respuestas al SUT.
Un ejecutor de pruebas se usa para ejecutar las pruebas y el informe los resultados de pruebas.
Las pruebas de integración, siga una secuencia de eventos que incluyen el habitual organizar, Act, y Assert
pasos de prueba:
1. Se configura el host de web del SUT.
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 en
según un esperado respuesta.
6. El proceso continúa hasta que todas las pruebas se ejecutan.
7. Se notifican los resultados de pruebas.
Por lo general, 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 diferentes.
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 pruebas y la ejecución.
El Microsoft.AspNetCore.Mvc.Testing paquete controla las tareas siguientes:
Copia el archivo de dependencias (*.deps) desde el SUT en el proyecto de prueba bin carpeta.
Establece la raíz del contenido en la raíz del proyecto del SUT para que se encuentran los archivos estáticos
y páginas o vistas cuando se ejecutan las pruebas.
Proporciona el WebApplicationFactory clase para simplificar el arranque del SUT con TestServer .
El pruebas unitarias documentación describe cómo configurar un proyecto y prueba el ejecutor de pruebas,
junto con 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
incluidas en las pruebas unitarias. Separación de las pruebas unitarias y de integración también permite controlar qué
conjunto de pruebas se ejecutan.

No hay prácticamente ninguna diferencia entre la configuración de pruebas de 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 puntos de conexión de 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, las pruebas normalmente se organizada por las clases de controlador y los
controladores que se prueban 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:
Hacer referencia a los siguientes paquetes:
Microsoft.AspNetCore.App
Microsoft.AspNetCore.Mvc.Testing
Especifique el SDK de Web en el archivo de proyecto ( <Project Sdk="Microsoft.NET.Sdk.Web"> ). El SDK de
Web es necesario cuando se hace referencia el Microsoft.AspNetCore.App metapaquete.
Estos requisitos previos se pueden ver en el aplicación de ejemplo. Inspeccionar el
tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj archivo. La aplicación de ejemplo usa el xUnit
marco de pruebas y la AngleSharp biblioteca analizador, por lo que también hace referencia a la aplicación de
ejemplo:
xUnit
xUnit.Runner.VisualStudio
AngleSharp

Pruebas básicas con el valor predeterminado WebApplicationFactory


WebApplicationFactory<TEntryPoint> se utiliza para crear un TestServer para las pruebas de integración.
TEntryPoint es la clase de punto de entrada del SUT, normalmente la Startup clase.

Clases de prueba implementan un accesorio clase interfaz ( IClassFixture ) para indicar la clase contiene las
pruebas y proporciona instancias de objetos compartidos entre las pruebas de la clase.
Prueba básica de los puntos de conexión de la aplicación
La siguiente clase, de prueba 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 200-299) y el
Content-Type encabezado es text/html; charset=utf-8 de varias páginas de aplicación.
CreateClient crea una instancia de HttpClient que sigue las 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 extremo seguro redirige a 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 redireccionamientos 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:


El código de estado devuelto por el SUT puede comprobarse 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 se
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 herencia
de WebApplicationFactory para crear uno o varios de los generadores personalizados:
1. Heredar de WebApplicationFactory e invalidar ConfigureWebHost. El IWebHostBuilder permite la
configuración de la colección de servicios 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 se realiza mediante 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. Usar personalizado CustomWebApplicationFactory en las clases de prueba. En el ejemplo siguiente se usa
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)
{
_factory = factory;
_client = factory.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
}

Cliente de la aplicación de ejemplo está configurada para evitar el HttpClient de redireccionamientos


siguientes. Como se explica en el probar un extremo seguro sección, esto permite que las 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. Una prueba normal se usa 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 al SUT debe cumplir la comprobación de antifalsificación se convierte


automáticamente en la aplicación sistema antifalsificación de protección de datos. Para organizar de la solicitud
POST de una prueba, la prueba de la aplicación debe:
1. Realice una solicitud para la página.
2. Analizar la cookie antifalsificación y el token de solicitud de validación de la respuesta.
3. Realizar la solicitud POST con la validación de solicitud y la cookie antifalsificación token en su lugar.
El SendAsyncmétodos de extensión de aplicación auxiliar (Helpers/HttpClientExtensions.cs) y el
GetDocumentAsync método auxiliar ( Helpers/HtmlHelpers.cs) en el deaplicacióndeejemplo utilizar el AngleSharp
analizador para controlar la comprobación de antifalsificación con los métodos siguientes:
GetDocumentAsync – Recibe el HttpResponseMessage y devuelve un IHtmlDocument . GetDocumentAsync
utiliza 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 el HttpClient redactar una HttpRequestMessage y llamar a
SendAsync(HttpRequestMessage) para enviar solicitudes al SUT. Sobrecargas para SendAsync acepte el
formulario HTML ( IHtmlFormElement ) y lo siguiente:
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 de formulario ( IEnumerable<KeyValuePair<string, string>> )

NOTE
AngleSharp es biblioteca que se usa para fines de demostración en este tema y la aplicación de ejemplo de análisis de un
tercero. AngleSharp no es compatible o necesarias para las pruebas de integración de aplicaciones de ASP.NET Core. Se
pueden usar otros analizadores, como el Html agilidad Pack (GRACIA). Otro enfoque consiste en escribir código para
controlar el token de comprobación de solicitud y la cookie antifalsificación el sistema antifalsificación directamente.

Personalización del cliente con WithWebHostBuilder


Cuando se necesita dentro de un método de prueba, configuración adicional WithWebHostBuilder crea un
nuevo WebApplicationFactory con un IWebHostBuilder que se personaliza más aún 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 elimina todos los registros en la base
de datos y puede ejecutar antes 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.
Seleccionar el deleteBtn1 botón de la messages se simula el formulario en el SUT en la solicitud al 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 7


máximo de respuestas de redirección
que HttpClient deben seguir las
instancias.

Crear el WebApplicationFactoryClientOptions clase y páselo a la CreateClient (método) (valor predeterminado,


los valores se muestran en el ejemplo de código):

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

Insertar servicios ficticios


Los servicios se pueden invalidar en una prueba con una llamada a ConfigureTestServices en el generador de
host. Para insertar servicios ficticios, debe tener el SUT un Startup clase con un
Startup.ConfigureServices método.

El ejemplo SUT incluye un servicio con ámbito que devuelve una cita. La oferta se incrusta en un campo oculto
en la página de índice cuando se solicita la página de índice.
Services/IQuoteService.cs:

public interface IQuoteService


{
Task<string> GenerateQuote();
}

Services/QuoteService.cs:

// Quote ©1975 BBC: The Doctor (Tom Baker); Dr. Who: Planet of Evil
// https://www.bbc.co.uk/programmes/p00pyrx6
public class QuoteService : IQuoteService
{
public Task<string> GenerateQuote()
{
return Task.FromResult<string>(
"Come on, Sarah. We've an appointment in London, " +
"and we're already 30,000 years late.");
}
}

Startup.cs:
services.AddScoped<IQuoteService, QuoteService>();

Páginas/Index.cshtml.cs:

public class IndexModel : PageModel


{
private readonly ApplicationDbContext _db;
private readonly IQuoteService _quoteService;

public IndexModel(ApplicationDbContext db, IQuoteService quoteService)


{
_db = db;
_quoteService = quoteService;
}

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

public IList<Message> Messages { get; private set; }

[TempData]
public string MessageAnalysisResult { get; set; }

public string Quote { get; private set; }

public async Task OnGetAsync()


{
Messages = await _db.GetMessagesAsync();

Quote = await _quoteService.GenerateQuote();


}

Pages/Index.cs:

<input id="quote" type="hidden" value="@Model.Quote">

Cuando se ejecuta la aplicación SUT, se genera el marcado siguiente:

<input id="quote" type="hidden" value="Come on, Sarah. We&#x27;ve an appointment in


London, and we&#x27;re already 30,000 years late.">

Para probar la inserción de la oferta y de servicio en una prueba de integración, un servicio de simulacro se
inserta en el SUT por la prueba. El servicio ficticio reemplaza la aplicación QuoteService con un servicio
proporcionado por la aplicación de prueba, llamado TestQuoteService :
IntegrationTests.IndexPageTests.cs:

// Quote ©1975 BBC: The Doctor (Tom Baker); Pyramids of Mars


// https://www.bbc.co.uk/programmes/p00pys55
public class TestQuoteService : IQuoteService
{
public Task<string> GenerateQuote()
{
return Task.FromResult<string>(
"Something's interfering with time, Mr. Scarman, " +
"and time is my business.");
}
}
ConfigureTestServices se llama a, y se registra el servicio con ámbito:

[Fact]
public async Task Get_QuoteService_ProvidesQuoteInPage()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddScoped<IQuoteService, TestQuoteService>();
});
})
.CreateClient();

//Act
var defaultPage = await client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
var quoteElement = content.QuerySelector("#quote");

// Assert
Assert.Equal("Something's interfering with time, Mr. Scarman, " +
"and time is my business.", quoteElement.Attributes["value"].Value);
}

El marcado generado durante la ejecución de la prueba refleja el texto de oferta proporcionado por
TestQuoteService , por lo tanto las fases de aserción:

<input id="quote" type="hidden" value="Something&#x27;s interfering with time,


Mr. Scarman, and time is my business.">

Cómo la infraestructura de pruebas deduce la ruta de acceso de


contenido raíz de aplicación
El WebApplicationFactory constructor infiere 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 recurre a la búsqueda de un
archivo de solución (*.sln) y anexa el TEntryPoint nombre del ensamblado en 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 es necesario establecer explícitamente la raíz de contenido de la aplicación,
como la lógica de búsqueda busca normalmente la raíz de contenido correcta en tiempo de ejecución. En
escenarios especiales donde 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 lógica
personalizada. Para establecer la raíz de contenido de la 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 , proporcionar 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 una personalizada WebApplicationFactory , heredan de


WebApplicationFactory e invalidar ConfigureWebHost:

public class CustomWebApplicationFactory<TStartup>


: WebApplicationFactory<RazorPagesProject.Startup>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
builder.UseSolutionRelativeContentRoot("<SOLUTION-RELATIVE-PATH>");

...
});
}
}

Deshabilitar las instantáneas


Las 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, deben deshabilitar las instantáneas. El aplicación de ejemplo usa
xUnit y deshabilita la copia sombra de 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 configuración xUnit.net con JSON.
Agregar el xunit.runner.json archivo a la 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 DEL PROYECTO DESCRIPCIÓN

Mensaje de aplicación (SUT) src/RazorPagesProject Permite al usuario agregar, eliminar


uno, elimine todo y analizar los
mensajes.

Aplicación de prueba tests/RazorPagesProject.Tests Utilizado para la prueba de integración


del SUT.

Se pueden ejecutar las pruebas con las características integradas de prueba de un IDE, como Visual Studio. Si
usa Visual Studio Code 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 ) del 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 de métodos de modelo para controlar la adición, eliminación y análisis de mensajes
(palabras medios por mensaje) .
Se describe un mensaje mediante la Message clase (Data/Message.cs) con dos propiedades: Id (clave) y
Text (mensaje). El Text propiedad es necesaria y limitada a 200 caracteres.
Los mensajes se almacenan mediante 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 solo sean accesibles para un usuario autenticado.
†El tema EF, pruebas con InMemory, se explica cómo usar una base de datos en memoria para las pruebas con
MSTest. Este tema se usa el xUnit marco de pruebas. Los conceptos de pruebas e implementaciones de prueba
a través de diferentes marcos son similares pero no idénticos.
Aunque la aplicación no usa el patrón de repositorio y no es un ejemplo eficaz de la patrón de unidades de
trabajo (UoW ), Razor Pages admite estos patrones de desarrollo. Para obtener más información, consulte
diseñar la capa de persistencia de infraestructura, Patrón de repositorio con ASP.NET Core, y lógica del
controlador de pruebas (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 custom
WebApplicationFactory clase.

Las aplicaciones auxiliares y 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 al SUT.

El marco de pruebas es xUnit. Las pruebas de integración se llevan a cabo mediante el


Microsoft.AspNetCore.TestHost, que incluye el TestServer. Dado que el Microsoft.AspNetCore.Mvc.Testing
paquete se usa para configurar el servidor host y pruebas de prueba, el TestHost y TestServer paquetes no
necesitan referencias de 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 de prueba
Las pruebas de integración suelen requieran un pequeño conjunto de datos en la base de datos antes de la
ejecución de pruebas. Por ejemplo, una eliminación probar las llamadas para la eliminación de registros de
base de datos, por lo que la base de datos debe tener al menos un registro para la solicitud de eliminación se
realice correctamente.
La aplicación de ejemplo inicializa la base de datos con tres mensajes en 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 unitarias páginas de Razor en ASP.NET
Core
23/08/2018 • 17 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 modelos de página ayudan a garantizar:
Partes de una aplicación de páginas de Razor funcionan independientemente y de forma conjunta como una
unidad durante la construcción de la aplicación.
Clases y métodos tienen una limitada a los ámbitos de responsabilidad.
Existe documentación adicional sobre cómo debe comportarse la aplicación.
Las regresiones, que son errores por actualizar el código, se encuentran durante la implementación y creación
automatizada.
En este tema se supone que tiene un conocimiento básico de aplicaciones de las páginas de Razor y pruebas
unitarias. Si no está familiarizado con los conceptos de pruebas o aplicaciones de las páginas de Razor, consulte
los temas siguientes:
Introducción a las páginas de Razor
Introducción a las páginas de Razor
Prueba unitaria de C# en .NET Core mediante pruebas de dotnet y xUnit
Vea o descargue el código de ejemplo (cómo descargarlo)
El proyecto de ejemplo se compone de dos aplicaciones:

APLICACIÓN CARPETA DEL PROYECTO DESCRIPCIÓN

Aplicación de mensaje src/RazorPagesTestSample Permite al usuario agregar, eliminar


uno, elimine todo y analizar los
mensajes.

Aplicación de prueba tests/RazorPagesTestSample.Tests Utilizado para la aplicación de mensajes


de pruebas unitarias: acceso a datos
(DAL) de la capa y el modelo de página
de índice.

Se pueden ejecutar las pruebas con las características integradas de prueba de un IDE, como Visual Studio. Si
usa Visual Studio Code 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 sencillo 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 de métodos de modelo para controlar la adición, eliminación y análisis de mensajes
(palabras medios por mensaje) .
Se describe un mensaje mediante la Message clase (Data/Message.cs) con dos propiedades: Id (clave) y
Text (mensaje). El Text propiedad es necesaria y limitada a 200 caracteres.
Los mensajes se almacenan mediante 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 propagar mensajes también se usan en las pruebas.
†El tema EF, pruebas con InMemory, se explica cómo usar una base de datos en memoria para las pruebas con
MSTest. Este tema se usa el xUnit marco de pruebas. Los conceptos de pruebas e implementaciones de prueba a
través de diferentes marcos son similares pero no idénticos.
Aunque la aplicación no usa el patrón de repositorio y no es un ejemplo eficaz de la patrón de unidades de
trabajo (UoW ), Razor Pages admite estos patrones de desarrollo. Para obtener más información, consulte
diseñar la capa de persistencia de infraestructura, Patrón de repositorio con ASP.NET Core, y lógica del
controlador de pruebas (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ágina de índice.

Utilidades Contiene el TestingDbContextOptions método utilizado


para crear nueva base de datos de opciones de contexto para
cada prueba unitaria DAL para que la base de datos se
restablece a su condición de la línea base para cada prueba.

El marco de pruebas es xUnit. El objeto de marco de simulación es Moq.

Pruebas unitarias de los datos de 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.

DeleteAllMessagesAsync Todos los elimina Message las entradas de la base de datos.


MÉTODO DAL FUNCIÓN

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 método para crear el DbContextOptions de cada prueba consiste en 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 cualquier estado de la prueba
anterior lo dejó. Esto puede ser problemático cuando se intenta escribir pruebas de unidad atómica que no
interfieren entre sí. Para forzar la AppDbContext para usar 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 hacerlo mediante 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 DAL pruebas permite que cada prueba ejecutar de forma atómica
con una instancia de base de datos actualizada:

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. ACT: Se ejecuta la prueba.
3. Aserción: 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 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 acomodar, donde realiza la preparación para el paso de Act.Los
mensajes de propagación se obtuvo y mantenidos en seedMessages . Los mensajes de propagación se guardan
en la base de datos. El mensaje con un Id de 1 está establecida para su eliminación. Cuando el
DeleteMessageAsync se ejecuta el método, los mensajes esperados deben tener todos los mensajes, excepto con
un 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 afirmación
de 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 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 que el número de mensajes 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 mediante 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 Las 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 el IndexPageTests clase
(tests/RazorPagesTestSample.Tests/UnitTests/IndexPageTests.cs). Las pruebas de usan el patrón de Act Assert
organizar familiar. 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 se realizan correctamente las asignaciones de valor de propiedad.
Este grupo de pruebas a menudo imitar los métodos de la capa DAL para generar los datos esperados para el
paso de Act donde se ejecuta un método del modelo de página. Por ejemplo, el GetMessagesAsync método de la
AppDbContext se simula para producir una salida. Cuando este método ejecuta en un método del modelo de
página, 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 del modelo de página.
El OnGetAsync_PopulatesThePageModel_WithAListOfMessages prueba muestra cómo el GetMessagesAsync se simula el
método para el modelo de página:

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 Act, llama el 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ágina 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 esta llamada al método. La versión del
método ficticio devuelve el resultado.
En el Assert paso, los mensajes reales ( actualMessages ) se asignan desde el Messages propiedad del modelo
de página. También se realiza una comprobación de tipo cuando se asignan los mensajes. Los mensajes
esperados y reales se comparan por 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 de modelo que incluyen la DefaultHttpContext , el
ModelStateDictionary , un ActionContext para establecer el PageContext , un ViewDataDictionary y un
PageContext . Estos son útiles para realizar pruebas. Por ejemplo, la aplicación de mensajes 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
Prueba unitaria de C# en .NET Core mediante pruebas de dotnet y xUnit
Controladores de pruebas
Prueba unitaria del código (Visual Studio)
Pruebas de integración
xUnit.net
Introducción a xUnit.net (.NET Core/ASP.NET Core)
Moq
Guía de inicio rápido de Moq
Probar la lógica del controlador en ASP.NET Core
31/08/2018 • 15 minutes to read • Edit Online

Por Steve Smith


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
Vea o descargue el código de ejemplo (cómo descargarlo)

Pruebas unitarias de la lógica del controlador


Las pruebas unitarias implican 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 va a escribir filtros personalizados y rutas, debería realizar pruebas unitarias en ellos de forma aislada, no
como parte de las pruebas de una acción de controlador concreta.

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
{
#region snippet_HomeController
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));


}
}
#endregion
}

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
{
#region snippet_Index_ReturnsAViewResult_WithAListOfBrainstormSessions
[Fact]
public async Task Index_ReturnsAViewResult_WithAListOfBrainstormSessions()
{
// Arrange
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.ListAsync())
.ReturnsAsync(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);
var redirectToActionResult = Assert.IsType<RedirectToActionResult>(result);
Assert.Null(redirectToActionResult.ControllerName);
Assert.Equal("Index", redirectToActionResult.ActionName);
mockRepo.Verify();
}
#endregion

#region snippet_GetTestSessions
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),

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.

}
#endregion

#region snippet_ModelState_ValidOrInvalid
[Fact]
public async Task IndexPost_ReturnsBadRequestResult_WhenModelStateIsInvalid()
{
// Arrange
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.ListAsync())
.ReturnsAsync(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

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
{
#region snippet_SessionController
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);
}
}
#endregion
}

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
{
#region snippet_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))
.ReturnsAsync((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))
.ReturnsAsync(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);
}
#endregion

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.Collections.Generic;
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 : ControllerBase
{
private readonly IBrainstormSessionRepository _sessionRepository;

public IdeasController(IBrainstormSessionRepository sessionRepository)


{
_sessionRepository = sessionRepository;
}

#region snippet_ForSessionAndCreate
[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)
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);
}
#endregion

#region snippet_ForSessionActionResult
[HttpGet("forsessionactionresult/{sessionId}")]
[ProducesResponseType(200)]
[ProducesResponseType(404)]
public async Task<ActionResult<List<IdeaDTO>>> ForSessionActionResult(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 result;
}
#endregion

#region snippet_CreateActionResult
[HttpPost("createactionresult")]
[ProducesResponseType(201)]
[ProducesResponseType(400)]
[ProducesResponseType(404)]
public async Task<ActionResult<BrainstormSession>> CreateActionResult([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()


var idea = new Idea()
{
DateCreated = DateTimeOffset.Now,
Description = model.Description,
Name = model.Name
};
session.AddIdea(idea);

await _sessionRepository.UpdateAsync(session);

return CreatedAtAction(nameof(CreateActionResult), new { id = session.Id }, session);


}
#endregion
}
}

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
de forma manual (con un método Select de LINQ como se muestra aquí) o mediante 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
{
#region snippet_ApiIdeasControllerTests1
[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);
}
#endregion

#region snippet_ApiIdeasControllerTests2
[Fact]
public async Task Create_ReturnsHttpNotFound_ForInvalidSession()
{
// Arrange
int testSessionId = 123;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync((BrainstormSession)null);
var controller = new IdeasController(mockRepo.Object);

// Act
var result = await controller.Create(new NewIdeaModel());

// Assert
Assert.IsType<NotFoundObjectResult>(result);
}
#endregion

#region snippet_ApiIdeasControllerTests3
[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))
.ReturnsAsync(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();
// Act
var result = await controller.ForSession(testSessionId);

// Assert
var okResult = Assert.IsType<OkObjectResult>(result);
var returnValue = Assert.IsType<List<IdeaDTO>>(okResult.Value);
var idea = returnValue.FirstOrDefault();
Assert.Equal("One", idea.Name);
}
#endregion

#region snippet_ForSessionActionResult_ReturnsNotFoundObjectResultForNonexistentSession
[Fact]
public async Task ForSessionActionResult_ReturnsNotFoundObjectResultForNonexistentSession()
{

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.

Recursos adicionales
Pruebas de integración en ASP.NET Core
Solución de problemas de proyectos de ASP.NET
Core
11/07/2018 • 4 minutes to read • Edit Online

Por Rick Anderson


Los vínculos siguientes proporcionan orientación para la solución:
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 (London, 2018): Diagnóstico de problemas en aplicaciones ASP.NET Core
Blog de ASP.NET: Solucionar problemas de rendimiento de ASP.NET Core

Advertencias de SDK de .NET core


Se instalan las versiones de 64 bits del SDK de .NET Core y 32 bits
En el nuevo proyecto cuadro de diálogo para ASP.NET Core, es posible que vea la advertencia siguiente:

Se instalan las versiones de 32 y 64 bits del SDK de .NET Core. 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 SDK de .NET Core están
instalados. Causas comunes que se pueden instalar ambas versiones se incluyen:
Descargar al instalador del SDK de .NET Core con un equipo de 32 bits pero, a continuación, copiarla en y
originalmente había instalado en un equipo de 64 bits.
Se instaló el SDK de .NET Core de 32 bits por otra aplicación.
Descargado e instalada la versión incorrecta.
Desinstalar el SDK de .NET Core de 32 bits para evitar esta advertencia. Desinstalar desde 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.
El SDK de .NET Core está instalado en varias ubicaciones
En el nuevo proyecto cuadro de diálogo para ASP.NET Core, es posible que vea la advertencia siguiente:

El SDK de .NET Core está instalado en varias ubicaciones. Sólo las plantillas de los SDK instalados en '
C:\archivos de programa\dotnet\sdk\' se mostrará.

Vea este mensaje cuando haya al menos una instalación de SDK de .NET Core en un directorio fuera de
C:\archivos de programa\dotnet\sdk\. Normalmente esto sucede cuando el SDK de .NET Core se ha
implementado en un equipo con copiar y pegar en lugar del instalador MSI.
Desinstalar el SDK de .NET Core de 32 bits para evitar esta advertencia. Desinstalar desde 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.
Se han detectado ningún SDK de .NET Core
En el nuevo proyecto cuadro de diálogo para ASP.NET Core, es posible que vea la advertencia siguiente:

Se han detectado 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 ningún SDK de .NET Core en el equipo.
Para resolver este problema:
Instalar o comprobar que está instalado el SDK de .NET Core.
Compruebe que el PATH variable de entorno se apunta a la ubicación donde está instalado el SDK. El instalador
se establece normalmente el PATH .
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
24/09/2018 • 29 minutes to read • Edit Online

La versión de ASP.NET Core 2.0 de este tutorial se puede encontrar en este archivo PDF.
La versión de ASP.NET Core 2.1 de este tutorial presenta muchas mejoras con respecto a la
versión 2.0.
Por Tom Dykstra y Rick Anderson
En la aplicación web de ejemplo Contoso University se muestra cómo crear una aplicación web
de Razor Pages de ASP.NET Core con Entity Framework (EF ) Core.
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
Visual Studio
CLI de .NET Core
Visual Studio 2017 versión 15.7.3 o posterior con las cargas de trabajo siguientes:
Desarrollo de ASP.NET y web
Desarrollo multiplataforma de .NET Core
SDK de .NET Core 2.1 o versiones posteriores
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 el proyecto completado. Una buena forma de obtener ayuda consiste
en publicar una pregunta en StackOverflow.com para ASP.NET Core o EF Core.

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 la aplicación web de Razor Pages


ContosoUniversity
Visual Studio
CLI de .NET Core
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.1 en la lista desplegable y, luego, Aplicación web.
Para ver las imágenes de los pasos anteriores, consulte Creación de una aplicación web de
Razor. Ejecute la aplicación.

Configurar el estilo del sitio


Con algunos cambios se configura el menú del sitio, el diseño y la página principal. Actualice
Pages/Shared/_Layout.cshtml con 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>

<partial name="_CookieConsentPartial" />

<div class="container body-content">


@RenderBody()
<hr />
<footer>
<p>&copy; 2018 : Contoso University</p>
</footer>
</div>

@*Remaining markup not shown for brevity.*@

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>

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. En el ejemplo anterior, la clave principal alternativa que se reconoce de
forma automática es StudentID .
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.

Aplicación de scaffolding al modelo de alumnos


En esta sección, se aplica scaffolding al modelo de alumnos. Es decir, la herramienta de
scaffolding genera páginas para las operaciones de creación, lectura, actualización y
eliminación (CRUD ) del modelo de alumnos.
Compile el proyecto.
Cree la carpeta Pages/Students.
Visual Studio
CLI de .NET Core
En el Explorador de soluciones, haga clic con el botón derecho en la carpeta
Pages/Students > Agregar > Nuevo elemento con scanffold.
En el cuadro de diálogo Agregar scaffold, seleccione Razor Pages using Entity
Framework (CRUD ) [Páginas de Razor Pages que usan Entity Framework (CRUD )] >
AGREGAR.
Complete el cuadro de diálogo para agregar páginas de Razor Pages que usan Entity
Framework (CRUD ):
En la lista desplegable Clase de modelo, seleccione Student
(ContosoUniversity.Models).
En la fila Clase de contexto de datos, haga clic en el signo + (más) y cambie el nombre
generado por ContosoUniversity.Models.SchoolContext.
En la lista desplegable Clase de contexto de datos, seleccione
ContosoUniversity.Models.SchoolContext
Seleccione Agregar.

Si tiene algún problema con el paso anterior, consulte Aplicar scaffolding al modelo de película.
El proceso de scaffolding ha creado y cambiado los archivos siguientes:
Archivos creados
Pages/Students Create, Delete, Details, Edit, Index.
Data/SchoolContext.cs
Actualizaciones de archivos
Startup.cs: en la sección siguiente se detallan los cambios realizados en este archivo.
appsettings.json: se agrega la cadena de conexión que se usa para conectarse a una base de
datos local.

Examinar el contexto registrado con la inserción de


dependencias
ASP.NET Core integra 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.
La herramienta de scaffolding creó de forma automática un contexto de base de datos y lo
registró con el contenedor de inserción de dependencias.
Examine el método ConfigureServices de Startup.cs. El proveedor de scaffolding ha agregado
la línea resaltada:
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);

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

El nombre de la cadena de conexión se pasa al contexto mediante una llamada a un método en


un objeto DbContextOptions. Para el desarrollo local, el sistema de configuración de ASP.NET
Core lee la cadena de conexión desde el archivo appsettings.json.

Actualización de main
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.
Llame a EnsureCreated.
Elimine el contexto cuando finalice el método EnsureCreated .
En el código siguiente se muestra el archivo Program.cs actualizado.
using ContosoUniversity.Models; // SchoolContext
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection; // CreateScope
using Microsoft.Extensions.Logging;
using System;

namespace ContosoUniversity
{
public class Program
{
public static void Main(string[] args)
{
var host = CreateWebHostBuilder(args).Build();

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


{
var services = scope.ServiceProvider;

try
{
var context = services.GetRequiredService<SchoolContext>();
context.Database.EnsureCreated();
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred creating the DB.");
}
}

host.Run();
}

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>


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

EnsureCreated garantiza la existencia de la base de datos para el contexto. Si existe, no se


realiza ninguna acción. Si no existe, se crean la base de datos y todo su esquema. En
EnsureCreated no se usan migraciones para crear la base de datos. Una base de datos que se
cree con EnsureCreated no se podrá actualizar más adelante mediante las migraciones.
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 .

EnsureCreated es útil al principio del desarrollo, cuando el esquema evoluciona rápidamente.


Más adelante, en el tutorial se elimina la base de datos y se usan las migraciones.
Prueba de la aplicación
Ejecute la aplicación y acepte la directiva de cookies. Esta aplicación no conserva información
de carácter personal. Puede obtener más información sobre la directiva de cookies en
Compatibilidad con el Reglamento general de protección de datos (RGPD ) de la UE.
Haga clic en el vínculo Students y, después, en Crear nuevo.
Pruebe los vínculos Edit, Details y Delete.

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

Actualice SchoolContext.cs con el código siguiente:

using Microsoft.EntityFrameworkCore;

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

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


public DbSet<Enrollment> Enrollment { get; set; }
public DbSet<Course> Course { get; set; }
}
}

El código resaltado crea una propiedad DbSet<TEntity > 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 .

SQL Server Express LocalDB


La cadena de conexión especifica 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 Initialize para
rellenarlo 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.Student.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.Student.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.Course.Add(c);
}
context.SaveChanges();

var enrollments = new Enrollment[]


{
new Enrollment{StudentID=1,CourseID=1050,Grade=Grade.A},
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.Enrollment.Add(e);
}
context.SaveChanges();
}
}
}

El código comprueba si hay estudiantes en la base de datos. Si no hay alumnos 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 llame a Initialize :

public class Program


{
public static void Main(string[] args)
{
var host = CreateWebHostBuilder(args).Build();

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


{
var services = scope.ServiceProvider;

try
{
var context = services.GetRequiredService<SchoolContext>();
// using ContosoUniversity.Data;
DbInitializer.Initialize(context);
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred creating the DB.");
}
}

host.Run();
}

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>


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

Elimine los registros de los alumnos y reinicie la aplicación. Si la base de datos no se ha


inicializado, establezca un punto de interrupción en Initialize para diagnosticar el problema.
Ver la base de datos
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.

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.Student.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 ,
FirstOrDefaultAsyncy 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 Programación
asincrónica y Programación asincrónica con async y await.
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
Esta es una recopilación de guías para aprovechar los servicios de Azure al máximo con ASP.NET Core.

Guías

DevOps con ASP.NET Core y Azure


Docs | PDF
DevOps con ASP.NET Core y Azure
12/09/2018 • 3 minutes to read • Edit Online

Por Cam Soper y Scott Addie


Esta guía está disponible como e-book descargable en formato PDF.

Pantalla de inicio
Le damos la bienvenida a la guía de Ciclo de vida de desarrollo de Azure para .NET. En esta guía le mostraremos
los conceptos básicos de creación de un ciclo de vida de desarrollo en torno a Azure con herramientas y procesos
de .NET. Cuando haya terminado con esta guía, podrá aprovechar las ventajas de una cadena de herramientas
madura de DevOps.

Destinatarios de esta guía


La guía está dirigida a desarrolladores experimentados de ASP.NET Core (nivel 200 o 300). No es necesario que
tenga conocimientos de Azure, ya que está incluido en esta introducción. Esta guía también podría ser útil para
ingenieros de DevOps, cuyo trabajo está más relacionado con las operación que con el desarrollo.
Esta guía está destinada a desarrolladores para Windows. Sin embargo, .NET Core es completamente compatible
con Linux y macOS. Para adaptar esta guía para Linux o macOS, mire las llamadas en las que se indican las
diferencias para Linux y macOS.

Aspectos no tratados en esta guía


Esta guía está centrada en una experiencia de desarrollo continuo de un extremo a otro para desarrolladores de
.NET. No es una guía exhaustiva de todo Azure y no se profundiza particularmente en API de .NET para servicios
de Azure. El énfasis está en la integración, la implementación, la supervisión y la depuración continuas. Casi al final
de la guía podrá ver recomendaciones para los pasos siguientes. En las sugerencias se incluyen servicios de
plataformas de Azure que resultan útiles para desarrolladores de ASP.NET Core.

Qué se incluye en esta guía


Herramientas y descargas
Obtenga información sobre dónde adquirir las herramientas que se usan en esta guía.
Implementación en App Service
Obtenga información sobre los distintos métodos para implementar una aplicación ASP.NET Core en Azure App
Service.
Integración e implementación continuas
Cree una solución de implementación e integración continuas de un extremo a otro para su aplicación ASP.NET
Core con GitHub, Azure DevOps Services y Azure.
Supervisión y depuración
Use las herramientas de Azure para supervisar la aplicación, solucionar problemas y ajustarla.
Pasos siguientes
Otras rutas de aprendizaje para los desarrolladores de ASP.NET Core que están aprendiendo sobre Azure.

Lecturas de introducción adicionales


Si se trata de su primer contacto con la informática en nube, en estos artículos aprenderá los conceptos básicos.
¿Qué es la informática en la nube?
Ejemplos de informática en la nube
¿Qué es una IaaS?
¿Qué es un PaaS?
Herramientas y descargas
11/09/2018 • 2 minutes to read • Edit Online

Azure tiene varias interfaces para aprovisionar y administrar los recursos, como el portal Azure, CLI de Azure,
Azure PowerShell, en la nube de Azure Shelly Visual Studio. Esta guía adopta un enfoque minimalista y usa Azure
Cloud Shell siempre que sea posible para reducir los pasos necesarios. Sin embargo, se debe usar el portal de
Azure para algunas partes.

Requisitos previos
Se requieren las siguientes suscripciones:
Azure — si no tienes una cuenta, obtener una evaluación gratuita.
Servicios de Azure DevOps — su suscripción de Azure DevOps y la organización se crea en el capítulo 4.
GitHub — si no tienes una cuenta, Regístrese gratuitamente.
Se requieren las siguientes herramientas:
GIT — se recomienda un entendimiento básico de Git en esta guía. Revise el documentación de Git,
concretamente git remoto y git push.
SDK de .NET core — versión 2.1.300 o posterior, es necesario para compilar y ejecutar la aplicación de
ejemplo. Si está instalado Visual Studio con el desarrollo multiplataforma de .NET Core ya está
instalada la carga de trabajo, el SDK de .NET Core.
Compruebe la instalación del SDK de .NET Core. Abra un shell de comandos y ejecute el siguiente
comando:

dotnet --version

Herramientas recomendadas (solo Windows)


Visual Studiodel sólidas herramientas de Azure proporcionan una interfaz gráfica de usuario para la
mayoría de la funcionalidad descrita en esta guía. Funcionará cualquier edición de Visual Studio, incluida la
edición gratuita de Visual Studio Community. Los tutoriales se escriben en demuestran el desarrollo,
implementación y DevOps con y sin Visual Studio.
Confirme que Visual Studio tiene las siguientes cargas de trabajo instalado:
Desarrollo web y ASP.NET
Desarrollo de Azure
Desarrollo multiplataforma de .NET Core
Implementar una aplicación en App Service
11/09/2018 • 15 minutes to read • Edit Online

Azure App Service es plataforma de hospedaje web de Azure. Implementar una aplicación web en Azure App
Service puede realizarse manualmente o mediante un proceso automatizado. En esta sección de la guía se describe
los métodos de implementación que se pueden desencadenar manualmente o mediante un script mediante la línea
de comandos, o desencadenan manualmente con Visual Studio.
En esta sección, podrá realizar las tareas siguientes:
Descargar y compilar la aplicación de ejemplo.
Crear una aplicación Web de Azure App Service mediante Azure Cloud Shell.
Implementar la aplicación de ejemplo en Azure con Git.
Implementar un cambio en la aplicación mediante Visual Studio.
Agregar una ranura de ensayo a la aplicación web.
Implementar una actualización en la ranura de ensayo.
Intercambiar las ranuras de ensayo y producción.

Descargar y probar la aplicación


La aplicación se usa en esta guía es una aplicación de ASP.NET Core pregenerada Simple lector de fuentes. Es una
aplicación de páginas de Razor que usa el Microsoft.SyndicationFeed.ReaderWriter API para recuperar una fuente
RSS/Atom y mostrar los elementos de noticias de una lista.
No dude en revisar el código, pero es importante entender que no hay nada especial acerca de esta aplicación. Es
simplemente una sencilla aplicación de ASP.NET Core con fines ilustrativos.
Desde un shell de comandos, descargar el código, compile el proyecto y ejecútelo como se indica a continuación.

Nota: Los usuarios de Linux/macOS deben realizar cambios correspondientes de las rutas de acceso, por
ejemplo, con barra diagonal ( / ) en lugar de barra diagonal inversa ( \ ).

1. Clone el código en una carpeta en el equipo local.

git clone https://github.com/Azure-Samples/simple-feed-reader/

2. Cambiar la carpeta de trabajo para el lectura de fuentes simple carpeta que se creó.

cd .\simple-feed-reader\SimpleFeedReader

3. Restaure los paquetes y compile la solución.

dotnet build

4. Ejecute la aplicación.

dotnet run
5. Abra un explorador y vaya a http://localhost:5000 . La aplicación le permite escribir o pegar una dirección
URL de fuente de distribución y ver una lista de elementos de noticias.

6. Cuando esté satisfecho la aplicación funciona correctamente, cierra presionando Ctrl+C en el shell de
comandos.

Crear la aplicación Web de Azure App Service


Para implementar la aplicación, debe crear un servicio de aplicaciones Web App. Tras la creación de la aplicación
Web, deberá implementar en él desde el equipo local mediante Git.
1. Inicie sesión en el Azure Cloud Shell. Nota: Al iniciar sesión por primera vez, Cloud Shell le insta a crear una
cuenta de almacenamiento para archivos de configuración. Acepte los valores predeterminados o
proporcione un nombre único.
2. Usar Cloud Shell para conocer los pasos siguientes.
a. Declare una variable para almacenar el nombre de la aplicación web. El nombre debe ser único para su
uso en la dirección URL predeterminada. Mediante el $RANDOM función Bash para construir el nombre
garantiza la exclusividad y da como resultado el formato webappname99999 .
webappname=mywebapp$RANDOM

b. Cree un grupo de recursos. Grupos de recursos proporcionan un medio para agregar los recursos de
Azure pueden administrarse como un grupo.

az group create --location centralus --name AzureTutorial

El az comando invoca el CLI de Azure. Se puede ejecutar la CLI localmente, pero usarlo en Cloud Shell
permite ahorrar tiempo y la configuración.
c. Crear un plan de App Service en el nivel S1. Un plan de App Service es una agrupación de aplicaciones
web que comparten el mismo plan de tarifa. El nivel de S1 no está disponible, pero es obligatorio para la
característica de espacios de almacenamiento provisional.

az appservice plan create --name $webappname --resource-group AzureTutorial --sku S1

d. Crear el recurso de aplicación web mediante el plan de App Service en el mismo grupo de recursos.

az webapp create --name $webappname --resource-group AzureTutorial --plan $webappname

e. Establecer las credenciales de implementación. Estas credenciales de implementación se aplican a todas


las aplicaciones web en su suscripción. No utilice caracteres especiales en el nombre de usuario.

az webapp deployment user set --user-name REPLACE_WITH_USER_NAME --password REPLACE_WITH_PASSWORD

f. Configurar la aplicación web para que acepte las implementaciones de local Git y mostrar el dirección URL
de la implementación de Git. Tenga en cuenta esta dirección URL para consultarla más tarde.

echo Git deployment URL: $(az webapp deployment source config-local-git --name $webappname --resource-
group AzureTutorial --query url --output tsv)

g. Mostrar el URL de aplicación web. Vaya a esta dirección URL para ver la aplicación web en blanco. Tenga
en cuenta esta dirección URL para consultarla más tarde.

echo Web app URL: http://$webappname.azurewebsites.net

3. Mediante un shell de comandos en el equipo local, vaya a la carpeta del proyecto de la aplicación web (por
ejemplo, .\simple-feed-reader\SimpleFeedReader ). Ejecute los comandos siguientes para configurar Git para
insertar en la dirección URL de implementación:
a. Agregue la dirección URL remota en el repositorio local.

git remote add azure-prod GIT_DEPLOYMENT_URL

b. Insertar local maestro bifurcar a la azure prod del remoto maestro rama.

git push azure-prod master

Se le pedirá las credenciales de implementación que creó anteriormente. Observe la salida en el shell de
comandos. Azure compila la aplicación de ASP.NET Core de forma remota.
4. En un explorador, vaya a la URL de aplicación Web y tenga en cuenta la aplicación se ha generado e
implementado. Cambios adicionales se pueden confirmados en el repositorio de Git local con git commit .
Estos cambios se insertan en Azure a la anterior git push comando.

Implementación con Visual Studio


Nota: Esta sección solo se aplica a Windows. Los usuarios de Linux y macOS deben realizar el cambio que se
describe en el paso 2 a continuación. Guarde el archivo y confirme el cambio en el repositorio local con
git commit . Por último, inserte el cambio con git push , como en la primera sección.

La aplicación ya se ha implementado desde el shell de comandos. Vamos a usar herramientas integradas de Visual
Studio para implementar una actualización en la aplicación. En segundo plano, Visual Studio realiza la misma tarea,
como las herramientas de línea de comandos, pero dentro de la interfaz de usuario familiar de Visual Studio.
1. Abra SimpleFeedReader.sln en Visual Studio.
2. En el Explorador de soluciones, abra Pages\Index.cshtml. Cambio <h2>Simple Feed Reader</h2> a
<h2>Simple Feed Reader - V2</h2> .

3. Presione Ctrl+MAYÚS+B para compilar la aplicación.


4. En el Explorador de soluciones, haga doble clic en el proyecto y haga clic en publicar.
5. Visual Studio puede crear un nuevo recurso de App Service, pero esta actualización se publicará a través de
la implementación existente. En el elegir un destino de publicación cuadro de diálogo, seleccione App
Service en la lista de la izquierda y, a continuación, seleccione seleccionar existente. Haga clic en
Publicar.
6. En el App Service cuadro de diálogo, confirme que Microsoft o cuenta profesional que se usa para crear la
suscripción de Azure se muestra en la esquina superior derecha. Si no es así, haga clic en la lista desplegable
y agréguelo.
7. Confirme que la Azure correcta suscripción está seleccionada. Para vista, seleccione grupo de recursos.
Expanda el AzureTutorial grupo de recursos y, a continuación, seleccione la aplicación web existente. Haga
clic en Aceptar.

Visual Studio genera e implementa la aplicación en Azure. Vaya a la dirección URL de aplicación web. Validar que
el <h2> modificación del elemento está en funcionamiento.
Ranuras de implementación
Ranuras de implementación admiten el almacenamiento provisional de los cambios sin afectar a la aplicación que
se ejecuta en producción. Una vez que un equipo de control de calidad se valida la versión de ensayo de la
aplicación, se pueden intercambiar espacios de ensayo y producción. La aplicación en ensayo se promueve a
producción de esta manera. Los siguientes pasos crean una ranura de ensayo, implementación algunos cambios en
él e intercambiar la ranura de ensayo a producción después de la comprobación.
1. Inicie sesión en el Azure Cloud Shell, si no ha iniciado sesión.
2. Creación de la ranura de ensayo.
a. Cree una ranura de implementación con el nombre ensayo.

az webapp deployment slot create --name $webappname --resource-group AzureTutorial --slot staging

b. Configurar el espacio de ensayo para usar la implementación de local Git y obtenga el ensayo dirección
URL de implementación. Tenga en cuenta esta dirección URL para consultarla más tarde.

echo Git deployment URL for staging: $(az webapp deployment source config-local-git --name $webappname -
-resource-group AzureTutorial --slot staging --query url --output tsv)

c. Mostrar la dirección URL de la ranura de ensayo. Vaya a la dirección URL para ver el espacio de ensayo
vacío. Tenga en cuenta esta dirección URL para consultarla más tarde.

echo Staging web app URL: http://$webappname-staging.azurewebsites.net

3. En un editor de texto o Visual Studio, modifique Pages/index.cshtml nuevo para que el <h2> lee el
elemento <h2>Simple Feed Reader - V3</h2> y guarde el archivo.
4. Confirmar el archivo en el repositorio de Git local, mediante el cambios página en Visual Studio Team
Explorer ficha, o escribiendo lo siguiente mediante el shell de comandos del equipo local:
git commit -a -m "upgraded to V3"

5. Mediante el shell de comandos de la máquina local, agregue la URL de la implementación de ensayo como
un Git remoto e insertar los cambios confirmados:
a. Agregue la dirección URL remota para el almacenamiento provisional en el repositorio de Git local.

git remote add azure-staging <Git_staging_deployment_URL>

b. Insertar local maestro bifurcar a la ensayo de azure del remoto maestro rama.

git push azure-staging master

Espere mientras Azure crea e implementa la aplicación.


6. Para comprobar que se ha implementado V3 en la ranura de ensayo, abra dos ventanas del explorador. En
una ventana, desplácese a la URL de aplicación web original. En la ventana, vaya a la URL de aplicación web
provisional. La dirección URL de producción actúa V2 de la aplicación. La URL de ensayo sirve V3 de la
aplicación.
7. En Cloud Shell, colocar la ranura de ensayo comprobado/preparado-up en producción.

az webapp deployment slot swap --name $webappname --resource-group AzureTutorial --slot staging

8. Compruebe que el intercambio se produjo al actualizar las ventanas del explorador de dos.
Resumen
En esta sección, se completaron las siguientes tareas:
Descargado y compilado la aplicación de ejemplo.
Crea una aplicación Web de Azure App Service mediante Azure Cloud Shell.
Implementar la aplicación de ejemplo en Azure mediante Git.
Implementar un cambio en la aplicación mediante Visual Studio.
Agrega una ranura de ensayo a la aplicación web.
Implementar una actualización en la ranura de ensayo.
Intercambiar las ranuras de ensayo y producción.
En la sección siguiente, obtendrá información sobre cómo crear una canalización de DevOps con canalizaciones de
Azure.
Lecturas adicionales
Introducción a Web Apps
Compilar una aplicación web de .NET Core y SQL Database en Azure App Service
Configurar credenciales de implementación para Azure App Service
Configurar entornos de ensayo en Azure App Service
Integración e implementación continuas
11/09/2018 • 22 minutes to read • Edit Online

En el capítulo anterior, ha creado un repositorio de Git local para la aplicación de lector de fuentes Simple. En este
capítulo, podrá publicar ese código en un repositorio de GitHub y crear una canalización de DevOps de Azure
Services mediante canalizaciones de Azure. La canalización permite compilaciones continuas y las
implementaciones de la aplicación. Cualquier confirmación en el repositorio de GitHub desencadena una
compilación y una implementación de la ranura de ensayo de la aplicación Web de Azure.
En esta sección, deberá completar las tareas siguientes:
Publicar el código de la aplicación en GitHub
Desconectar de la implementación de Git local
Creación de una organización de Azure DevOps
Crear un proyecto de equipo en servicios de Azure DevOps
Crear una definición de compilación
Crear una canalización de versiones
Confirmar cambios en GitHub e implementar automáticamente en Azure
Examine la canalización de canalizaciones de Azure

Publicar el código de la aplicación en GitHub


1. Abra una ventana del explorador y vaya a https://github.com .
2. Haga clic en el + desplegable en el encabezado y seleccione nuevo repositorio:

3. Seleccione su cuenta en el propietario lista desplegable y escriba lectura de fuentes simple en el nombre
del repositorio cuadro de texto.
4. Haga clic en el crear repositorio botón.
5. Abra el shell de comandos del equipo local. Navegue hasta el directorio en el que el lectura de fuentes
simple se almacena el repositorio de Git.
6. Cambiar el nombre existente origen remoto a ascendente. Ejecute el siguiente comando:

git remote rename origin upstream

7. Agregue un nuevo origen remoto que apunta a la copia del repositorio en GitHub. Ejecute el siguiente
comando:
git remote add origin https://github.com/<GitHub_username>/simple-feed-reader/

8. Publicar repositorio Git local en el repositorio de GitHub recién creado. Ejecute el siguiente comando:

git push -u origin master

9. Abra una ventana del explorador y vaya a https://github.com/<GitHub_username>/simple-feed-reader/ .


Validar que el código aparece en el repositorio de GitHub.

Desconectar de la implementación de Git local


Quite la implementación de Git local con los pasos siguientes. Las canalizaciones de Azure (un servicio de Azure
DevOps) tanto reemplaza y amplía dicha funcionalidad.
1. Abra el portal de Azurey navegue hasta el de almacenamiento provisional
(mywebapp<unique_number>/ensayo ) Web App. La aplicación Web se pueden ubicar rápidamente
escribiendo ensayo en el cuadro de búsqueda del portal:

2. Haga clic en opciones de implementación. Aparece un nuevo panel. Haga clic en desconexión para
quitar la configuración de control de código fuente de Git local que se agregó en el capítulo anterior.
Confirmar la operación de eliminación, haga clic en el Sí botón.
3. Navegue hasta la mywebapp < unique_number > App Service. Como recordatorio, cuadro de búsqueda del
portal puede utilizarse para localizar rápidamente el servicio de aplicación.
4. Haga clic en opciones de implementación. Aparece un nuevo panel. Haga clic en desconexión para
quitar la configuración de control de código fuente de Git local que se agregó en el capítulo anterior.
Confirmar la operación de eliminación, haga clic en el Sí botón.

Creación de una organización de Azure DevOps


1. Abra un explorador y navegue hasta la página de creación de organización de Azure DevOps.
2. Escriba un nombre único en la elegir un nombre fácil de recordar textbox para formar la dirección URL
para tener acceso a su organización de DevOps de Azure.
3. Seleccione el Git botón de radio, puesto que el código se hospeda en un repositorio de GitHub.
4. Haga clic en el botón Continuar. Tras una breve espera, una cuenta y un proyecto de equipo, nombre
MyFirstProject, se crean.
5. Abra el correo electrónico de confirmación que indica que la organización de DevOps de Azure y el
proyecto están listos para su uso. Haga clic en el comience su proyecto botón:

6. Se abre un explorador para <account_name>. visualstudio.com. Haga clic en el MyFirstProject vínculo para
empezar a configurar la canalización de DevOps del proyecto.

Configurar la canalización de canalizaciones de Azure


Hay tres pasos diferentes para completar. Completando los pasos descritos en los resultados de tres secciones
siguientes en una canalización de DevOps operativa.
DevOps de Azure conceder acceso al repositorio de GitHub
1. Expanda el o compilar código desde un repositorio externo accordion. Haga clic en el compilar el
programa de instalación botón:

2. Seleccione el GitHub opción desde el seleccionar un origen de sección:


3. Se necesita autorización antes de DevOps de Azure puede acceder al repositorio de GitHub. Escriba <
GitHub_username > GitHub conexión en el nombre de la conexión cuadro de texto. Por ejemplo:

4. Si está habilitada la autenticación en dos fases en su cuenta de GitHub, se requiere un token de acceso
personal. En ese caso, haga clic en el autorizar con un token de acceso personal de GitHub vínculo.
Consulte la instrucciones oficiales de creación del token de acceso personal de GitHub para obtener ayuda.
Solo el repositorio ámbito de permisos es necesaria. En caso contrario, haga clic en el autorizar el uso de
OAuth botón.
5. Cuando se le solicite, inicie sesión en su cuenta de GitHub. A continuación, seleccione autorizar para
conceder acceso a su organización de DevOps de Azure. Si se realiza correctamente, se crea un nuevo
extremo de servicio.
6. Haga clic en el botón de puntos suspensivos junto a la repositorio botón. Seleccione el <
GitHub_username > / lectura de fuentes simple repositorio en la lista. Haga clic en el seleccione botón.
7. Seleccione el maestro bifurcación desde la rama predeterminada para compilaciones manuales y
programadas lista desplegable. Haga clic en el botón Continuar. Aparece la página de selección de
plantilla.
Crear la definición de compilación
1. En la página de selección de plantilla, escriba ASP.NET Core en el cuadro de búsqueda:

2. Aparecen los resultados de búsqueda de la plantilla. Mantenga el mouse sobre el ASP.NET Core plantilla y
haga clic en el aplicar botón.
3. El tareas aparecerá la pestaña de la definición de compilación. Haga clic en el desencadenadores ficha.
4. Compruebe el habilitar la integración continua cuadro. En el filtros de rama , confirme que la tipo
desplegable se establece en Include. Establecer el especificación de rama lista desplegable para maestro.
Esta configuración hace que una compilación desencadenar cuando se inserta cualquier cambio en el
maestro rama del repositorio de GitHub. La integración continua se prueba en el confirmar los cambios en
GitHub e implementar automáticamente en Azure sección.
5. Haga clic en el guardar y poner en cola botón y seleccione el guardar opción:

6. Aparece el cuadro de diálogo modal siguiente:

Use la carpeta predeterminada de *\*y haga clic en el guardar botón.


Crear la canalización de versiones
1. Haga clic en el versiones ficha del proyecto de equipo. Haga clic en el nueva canalización botón.
Aparece el panel de selección de plantilla.
2. En la página de selección de plantilla, escriba App Service en el cuadro de búsqueda:

3. Aparecen los resultados de búsqueda de la plantilla. Mantenga el mouse sobre el implementación de


Azure App Service con espacio plantilla y haga clic en el aplicar botón. El canalización aparecerá la
pestaña de la canalización de versiones.

4. Haga clic en el agregar situado en la artefactos cuadro. El agregar artefacto aparecerá el panel:
5. Seleccione el compilar icono desde la tipo de origen sección. Este tipo permite la vinculación de la
canalización de versiones para la definición de compilación.
6. Seleccione MyFirstProject desde el proyecto lista desplegable.
7. Seleccione el nombre de la definición de compilación, MyFirstProject ASP.NET Core-CI, desde el origen
(definición de compilación) lista desplegable.
8. Seleccione más reciente desde el versión predeterminada lista desplegable. Esta opción crea los
artefactos producidos por la ejecución más reciente de la definición de compilación.
9. Reemplace el texto en el alias de origen textbox con Drop.
10. Haga clic en el botón Agregar. El artefactos sección se actualizará para mostrar los cambios.
11. Haga clic en el icono de rayo para permitir implementaciones continuas:

Con esta opción habilitada, una implementación se produce cada vez que está disponible una nueva
compilación.
12. Un desencadenador de implementación continua panel situado a la derecha. Haga clic en el botón de
alternancia para habilitar la característica. No es necesario habilitar el desencadenador de solicitud de
incorporación de cambios.
13. Haga clic en el agregar desplegable en el Generar filtros de rama sección. Elija la rama predeterminada
de la definición de compilación opción. Este filtro hace que la versión desencadenar una compilación
desde el repositorio de GitHub solo maestro rama.
14. Haga clic en el botón Guardar. Haga clic en el Aceptar botón en el cuadro guardar cuadro de diálogo
modal.
15. Haga clic en el entorno 1 cuadro. Un entorno panel situado a la derecha. Cambiar el entorno 1 texto en el
nombre del entorno textbox a producción.

16. Haga clic en el 1 fase, 2 tareas vincular en el producción cuadro:

El tareas aparecerá la pestaña del entorno.


17. Haga clic en el implementar Azure App Service para la ranura tarea. Su configuración aparece en un
panel a la derecha.
18. Seleccione la suscripción asociada con el servicio de aplicación desde el suscripción de Azure lista
desplegable. Una vez seleccionado, haga clic en el Authorize botón.
19. Seleccione Web App desde el tipo de aplicación lista desplegable.
20. Seleccione mywebapp / < unique_number / > desde el nombre de App service lista desplegable.
21. Seleccione AzureTutorial desde el grupo de recursos lista desplegable.
22. Seleccione ensayo desde el ranura lista desplegable.
23. Haga clic en el botón Guardar.
24. Mantenga el mouse sobre el nombre de canalización de versión predeterminado. Haga clic en el icono de
lápiz para editarlo. Use MyFirstProject ASP.NET Core-CD como el nombre.
25. Haga clic en el botón Guardar.

Confirmar cambios en GitHub e implementar automáticamente en


Azure
1. Abra SimpleFeedReader.sln en Visual Studio.
2. En el Explorador de soluciones, abra Pages\Index.cshtml. Cambio <h2>Simple Feed Reader - V3</h2> a
<h2>Simple Feed Reader - V4</h2> .

3. Presione Ctrl+MAYÚS+B para compilar la aplicación.


4. Confirmar el archivo en el repositorio de GitHub. Usar el cambios página en Visual Studio Team Explorer
pestaña o ejecute lo siguiente mediante el shell de comandos de la máquina local:

git commit -a -m "upgraded to V4"

5. Inserte el cambio el maestro bifurcar a la origen remoto del repositorio de GitHub:

git push origin master

La confirmación que aparece en el repositorio de GitHub maestro rama:

Se desencadena la compilación, ya que está habilitada la integración continua en la definición de


compilación desencadenadores pestaña:

6. Navegue hasta la en cola pestaña de la canalizaciones de Azure > compilaciones página en los
servicios de Azure DevOps. La compilación en cola muestra la rama y confirmar que desencadenó la
compilación:

7. Una vez que la compilación se realiza correctamente, se produce una implementación en Azure. Vaya a la
aplicación en el explorador. Tenga en cuenta que el texto "V4" aparece en el encabezado:
Examine la canalización de canalizaciones de Azure
Definición de compilación
Se creó una definición de compilación con el nombre MyFirstProject ASP.NET Core-CI. Tras la finalización, la
compilación genera un .zip archivo, incluidos los activos a publicarse. La canalización de versiones implementa
esos recursos en Azure.
La definición de compilación tareas pestaña enumera los pasos individuales que se va a usar. Hay cinco tareas de
compilación.

1. Restaurar — ejecuta el dotnet restore comando para restaurar los paquetes de NuGet de la aplicación. El
paquete predeterminado es de fuente utilizado nuget.org.
2. Compilar — ejecuta el dotnet build --configuration release comando para compilar el código de la
aplicación. Esto --configuration opción se usa para generar una versión optimizada del código, que es
adecuado para su implementación en un entorno de producción. Modificar el BuildConfiguration variable
en la definición de compilación Variables pestaña si, por ejemplo, se necesita una configuración de
depuración.
3. Prueba — ejecuta el
dotnet test --configuration release --logger trx --results-directory <local_path_on_build_agent>
comando para ejecutar pruebas unitarias de la aplicación. Las pruebas unitarias se ejecutan dentro de
cualquier C# proyecto coincidencia la **/*Tests/*.csproj patrón global. Los resultados de pruebas se
guardan en un .trx archivo en la ubicación especificada por el --results-directory opción. Si se produce un
error en las pruebas, la compilación se produce un error y no está implementada.

NOTE
Para comprobar el trabajo de pruebas unitarias, modificar SimpleFeedReader.Tests\Services\NewsServiceTests.cs
intencionadamente interrumpir una de las pruebas. Por ejemplo, cambiar Assert.True(result.Count > 0); a
Assert.False(result.Count > 0); en el Returns_News_Stories_Given_Valid_Uri método. Confirme e inserte el
cambio a GitHub. La compilación se desencadena y se produce un error. El estado de la canalización de compilación
cambia a no se pudo. Revertir el cambio, confirmarlo e insertarlo de nuevo. La compilación se realiza correctamente.

4. Publicar — ejecuta el dotnet publish --configuration release --output <local_path_on_build_agent>


comando para generar un .zip archivo con los artefactos que se va a implementarse. El --output opción
especifica la ubicación de publicación de la .zip archivo. Que la ubicación se especifica pasando una variable
predefinida denominado $(build.artifactstagingdirectory) . Esa variable se expande a una ruta de acceso
local, como c:\agent_work\1\a, en el agente de compilación.
5. Publicar artefacto — Publishes el .zip archivo generado por el publicar tarea. La tarea acepta el .zip
ubicación como un parámetro, que es la variable predefinida del archivo $(build.artifactstagingdirectory) .
El .zip archivo se publica como una carpeta denominada drop.
Haga clic en la definición de compilación resumen vínculo para ver un historial de compilaciones con la definición
de:

En la página resultante, haga clic en el vínculo correspondiente al número de compilación único:

Se muestra un resumen de esta compilación concreta. Haga clic en el artefactos pestaña y observe el drop carpeta
generado por la compilación se muestra:
Use la descargar y explorar vínculos para inspeccionar los artefactos publicados.
Canalización de versiones
Se creó una canalización de versiones con el nombre MyFirstProject ASP.NET Core-CD:

Los dos componentes principales de la canalización de versiones son el artefactos y entornos. Al hacer clic en el
cuadro en el artefactos sección, muestra el panel siguiente:

El origen (definición de compilación) valor representa la definición de compilación al que está vinculada esta
canalización de versiones. El .zip archivo generado por una ejecución correcta de la definición de compilación se
proporciona para el producción entorno para la implementación en Azure. Haga clic en el 1 fase, 2 tareas vincular
en el producción cuadro del entorno para ver las tareas de canalización de versión:
La canalización de versiones consta de dos tareas: implementar Azure App Service para la ranura y administrar
Azure App Service: intercambio de ranura. Al hacer clic en la primera tarea, se muestra la configuración de la tarea
siguiente:

La suscripción de Azure, tipo de servicio, nombre de la aplicación web, grupo de recursos y ranura de
implementación se definen en la tarea de implementación. El paquete o carpeta cuadro de texto contiene el .zip
ruta de acceso de archivo al extraer e implementar a la ensayo ranura de la mywebapp<único _número>
aplicación web.
Al hacer clic en la tarea de intercambio de ranura, se muestra la configuración de la tarea siguiente:
La suscripción, grupo de recursos, tipo de servicio, nombre de la aplicación web y detalles de la ranura de
implementación se proporcionan. El intercambiar con producción está activada la casilla de verificación. Por lo
tanto, los bits implementan para el ensayo ranura entran en el entorno de producción.

Lecturas adicionales
Crear su primera canalización con canalizaciones de Azure
Proyecto de compilación y .NET Core
Implementar una aplicación web con canalizaciones de Azure
Supervisar y depurar
27/08/2018 • 10 minutes to read • Edit Online

Al haber implementado la aplicación y crea una canalización de DevOps, es importante comprender cómo
supervisar y solucionar problemas de la aplicación.
En esta sección, deberá completar las tareas siguientes:
Buscar básicas de supervisión y solución de problemas de datos en Azure portal
Obtenga información sobre cómo Azure Monitor proporciona una visión más profunda de las métricas en
todos los servicios de Azure
Conectar la aplicación web con Application Insights para la generación de perfiles de aplicación
Activar el registro y obtenga información sobre dónde descargar los registros
Stream se registra en tiempo real
Obtenga información sobre dónde establecer alertas
Obtenga información acerca de Azure App Service web apps depuración remotas.

Supervisión básica y solución de problemas


App Service web apps se supervisan con facilidad en tiempo real. El portal de Azure procesa las métricas en
gráficos fáciles de entender y gráficos.
1. Abra el portal de Azurey, a continuación, navegue hasta la mywebapp<unique_number> App Service.
2. El Introducción ficha muestra información de "rápida" útil, incluidos gráficos de mostrar las métricas
recientes.
Http 5xx: recuento de errores de servidor, normalmente excepciones en el código de ASP.NET Core.
Datos de entrada: entrada de datos que entran en la aplicación web.
Datos de salida: datos de la salida desde la aplicación web a los clientes.
Las solicitudes: número de solicitudes.
Tiempo promedio de respuesta: promedio de tiempo para la aplicación web responder a las
solicitudes HTTP.
También se encuentran varias herramientas de autoservicio para la optimización y solución de problemas
en esta página.

Diagnosticar y resolver problemas es un solucionador de problemas de autoservicio.


Application Insights es para la generación de perfiles de rendimiento y el comportamiento de la
aplicación y se describe más adelante en esta sección.
App Service Advisor hace recomendaciones para optimizar su experiencia de aplicación.

Supervisión avanzada
Azure Monitor es el servicio centralizado para todas las métricas de supervisión y configuración de alertas a través
de servicios de Azure. Dentro de Azure Monitor, los administradores pueden realizar un seguimiento de
rendimiento forma granular e identificar tendencias. Cada servicio de Azure ofrece su propio conjunto de métricas
a Azure Monitor.

Perfil con Application Insights


Application Insights es un servicio de Azure para analizar el rendimiento y la estabilidad de las aplicaciones web y
cómo usar los usuarios. Los datos de Application Insights son más amplias y profundas que el de Azure Monitor.
Los datos pueden proporcionar a los desarrolladores y administradores con información de clave para mejorar las
aplicaciones. Application Insights pueden agregarse a un recurso de Azure App Service sin cambios de código.
1. Abra el portal de Azurey, a continuación, navegue hasta la mywebapp<unique_number> App Service.
2. Desde el Introducción pestaña, haga clic en el Application Insights icono.

3. Seleccione el crear recurso nuevo botón de radio. Use el nombre de recurso predeterminado y seleccione
la ubicación del recurso de Application Insights. La ubicación no tiene que coincidir con el de la aplicación
web.

4. Para en tiempo de ejecución o marco, seleccione ASP.NET Core. Acepte la configuración


predeterminada.
5. Seleccione Aceptar. Si se le pedirá que confirme, seleccione continuar.
6. Una vez creado el recurso, haga clic en el nombre del recurso de Application Insights para navegar
directamente a la página de Application Insights.

Se usa la aplicación, se acumulan datos. Seleccione actualizar para volver a cargar la hoja con nuevos datos.
Application Insights proporciona información útil de servidor sin ninguna configuración adicional. Para obtener el
máximo partido de Application Insights, instrumentar la aplicación con el SDK de Application Insights. Cuando se
configura correctamente, el servicio proporciona la supervisión de extremo a otro entre el servidor web y el
explorador, incluidos el rendimiento del cliente. Para obtener más información, consulte el documentación de
Application Insights.

Registro
Los registros de aplicación y el servidor Web están deshabilitados de forma predeterminada en Azure App Service.
Habilitar los registros con los pasos siguientes:
1. Abra el portal de Azurey navegue hasta la mywebapp<unique_number> App Service.
2. En el menú a la izquierda, desplácese hacia abajo hasta la supervisión sección. Seleccione los registros de
diagnóstico.

3. Activar registro de la aplicación (Filesystem ). Si se le solicite, haga clic en el cuadro para instalar las
extensiones para habilitar el registro en la aplicación web de aplicación.
4. Establecer registro del servidor Web a sistema de archivos.
5. Escriba el período de retención en días. Por ejemplo, 30.
6. Haga clic en Guardar.
Se generan registros de servidor (App Service) de ASP.NET Core y web para la aplicación web. Se pueden
descargar mediante la información de FTP o FTPS que aparece. La contraseña es el mismo que las credenciales de
implementación que creó anteriormente en esta guía. Los registros pueden ser transmite directamente a la
máquina local con PowerShell o CLI de Azure. Los registros también pueden ser ven en Application Insights.

Secuencias de registro
Registros de servidor web y de aplicación se pueden transmitir en tiempo real a través del portal.
1. Abra el portal de Azurey navegue hasta la mywebapp<unique_number> App Service.
2. En el menú a la izquierda, desplácese hacia abajo hasta la supervisión sección y seleccione secuencia de
registro.

Los registros también pueden ser transmite a través de CLI de Azure o Azure PowerShell, incluido a través de
Cloud Shell.

Alertas
Azure Monitor proporciona también alertas en tiempo real basadas en métricas, eventos administrativos y otros
criterios.

Nota: Solo está disponible en el servicio de alertas (clásico ) actualmente alertas de métricas de la aplicación
web.
El alertas (clásico) servicio puede encontrarse en Azure Monitor o en el supervisión sección de la configuración
de App Service.

Depuración activa
Azure App Service puede ser depurar de manera remota con Visual Studio cuando los registros no proporcionan
suficiente información. Sin embargo, la depuración remota requiere que la aplicación para compilarse con
símbolos de depuración. Depuración no debería realizarse en producción, excepto como último recurso.

Conclusión
En esta sección, se completó las tareas siguientes:
Buscar básicas de supervisión y solución de problemas de datos en Azure portal
Obtenga información sobre cómo Azure Monitor proporciona una visión más profunda de las métricas en
todos los servicios de Azure
Conectar la aplicación web con Application Insights para la generación de perfiles de aplicación
Activar el registro y obtenga información sobre dónde descargar los registros
Stream se registra en tiempo real
Obtenga información sobre dónde establecer alertas
Obtenga información acerca de Azure App Service web apps depuración remotas.

Lecturas adicionales
Solución de problemas de ASP.NET Core en Azure App Service
Referencia de errores comunes de Azure App Service e IIS con ASP.NET Core
Supervisar el rendimiento de la aplicación web de Azure con Application Insights
Habilitar el registro de diagnósticos para las aplicaciones web en Azure App Service
Solución de problemas de una aplicación web en Azure App Service con Visual Studio
Creación de alertas de métricas clásicas en Azure Monitor para servicios de Azure - portal de Azure
Pasos siguientes
27/08/2018 • 3 minutes to read • Edit Online

En esta guía, ha creado una canalización de DevOps para una aplicación de ejemplo de ASP.NET Core.
¡ Enhorabuena! Esperamos que haya disfrutado de aprendizaje publicar aplicaciones web ASP.NET Core en Azure
App Service y automatizar la integración continua de los cambios.
Más allá de hospedaje web y DevOps, Azure tiene una amplia gama de servicios de plataforma como-servicio
(PaaS ) útiles para los desarrolladores de ASP.NET Core. En esta sección se ofrece una breve descripción de
algunos de los servicios más usados.

Almacenamiento y bases de datos


Caché en Redis está disponible como un servicio de almacenamiento en caché de datos de alto rendimiento y baja
latencia. Se puede usar para el almacenamiento en caché de resultados de la página, reducir las solicitudes de base
de datos y proporcionar el estado de sesión de ASP.NET Core en varias instancias de una aplicación.
Almacenamiento de Azure es escalable a gran escala en la nube el almacenamiento de Azure. Los desarrolladores
pueden aprovechar las ventajas de Queue Storage para poner en cola confiable de mensajes, y Table Storage es un
almacén de pares clave-valor NoSQL diseñado para agiliza el desarrollo con conjuntos de datos semiestructurados
masivos.
La base de datos de SQL Azure proporciona la funcionalidad de la base de datos relacional como servicio
mediante el motor de Microsoft SQL Server.
COSMOS DB servicio de base de datos NoSQL de varios modelo distribuido globalmente. Varias API están
disponibles, incluidas las API de SQL (anteriormente denominado DocumentDB ), Cassandra y MongoDB.

identidad
Azure Active Directory y Azure Active Directory B2C son ambos servicios de identidad. Azure Active Directory está
diseñado para escenarios empresariales y permite la colaboración de Azure AD B2B (negocio a negocio), mientras
que Azure Active Directory B2C es el previsto de los escenarios de cliente de empresa, incluidos los inicio de
sesión de red social.

Móvil
Notification Hubs es un motor de notificaciones de inserción multiplataforma y escalable para enviar rápidamente
millones de mensajes a aplicaciones que se ejecutan en distintos tipos de dispositivos.

Infraestructura Web
Azure Container Service administra el entorno hospedado de Kubernetes, lo que rápida y fácil de implementar y
administrar aplicaciones en contenedores sin tener conocimientos de orquestación de contenedores.
Azure Search se usa para crear una solución de búsqueda empresarial sobre contenido privado y heterogéneo.
Service Fabric es una plataforma de sistemas distribuidos que facilita la tarea empaquetar, implementar y
administrar escalable y confiable microservicios y contenedores.
Desarrollo del lado cliente en ASP.NET Core
03/09/2018 • 2 minutes to read • Edit Online

Uso de Gulp
Uso de Grunt
Uso de LibMan
CLI de LibMan
LibMan en Visual Studio
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
Adquisición de bibliotecas del lado cliente en
ASP.NET Core con LibMan
31/08/2018 • 2 minutes to read • Edit Online

Por Scott Addie


Library Manager (LibMan) es una herramienta ligera para la adquisición de bibliotecas del lado cliente.
Asimismo, LibMan permite descargar bibliotecas y marcos populares de sistemas de archivos o redes de
entrega de contenido (CDN ). Entre las CDN admitidas se incluyen CDNJS y unpkg. Los archivos de biblioteca
seleccionados se capturan y se colocan en la ubicación adecuada dentro del proyecto de ASP.NET Core.

Casos de uso de LibMan


LibMan ofrece las ventajas siguientes:
Solo se descargan los archivos de biblioteca necesarios.
No es necesario usar otras herramientas como Node.js, npm o WebPack para adquirir un subconjunto de
archivos de una biblioteca.
Los archivos se pueden colocar en una ubicación específica sin tener que usar tareas de compilación ni
copiar archivos manualmente.
Para obtener más información sobre las ventajas del LibMan, vea Modern front-end web development in Visual
Studio 2017: LibMan segment (Desarrollo web front-end moderno en Visual Studio 2017: segmento LibMan).
LibMan no es un sistema de administración de paquetes. Si ya usa un administrador de paquetes, por ejemplo,
npm o yarn, le recomendamos que sigua haciéndolo. LibMan no se ha desarrollado para reemplazar dichas
herramientas.

Recursos adicionales
Usar LibMan con ASP.NET Core en Visual Studio
Usar la interfaz de línea de comandos (CLI) de LibMan con ASP.NET Core
Repositorio de LibMan en GitHub
Usar la interfaz de línea de comandos (CLI) de
LibMan con ASP.NET Core
27/09/2018 • 14 minutes to read • Edit Online

Por Scott Addie


El LibMan CLI es una herramienta multiplataforma que se admite en todas partes .NET Core es compatible.

Requisitos previos
SDK de .NET Core 2.1 o versiones posteriores

Instalación
Para instalar la CLI LibMan:

dotnet tool install -g Microsoft.Web.LibraryManager.Cli

Un herramienta Global de .NET Core se instala desde el Microsoft.Web.LibraryManager.Cli paquete NuGet.


Para instalar la CLI LibMan desde un origen de paquete de NuGet específico:

dotnet tool install -g Microsoft.Web.LibraryManager.Cli --version 1.0.94-g606058a278 --add-source C:\Temp\

En el ejemplo anterior, se instala una herramienta Global de .NET Core desde la máquina Windows local
C:\Temp\Microsoft.Web.LibraryManager.Cli.1.0.94 -g606058a278.nupkg archivo.

Uso
Tras una instalación correcta de la CLI, se puede usar el comando siguiente:

libman

Para ver la versión instalada de CLI:

libman --version

Para ver los comandos CLI disponibles:

libman --help

El comando anterior muestra un resultado similar al siguiente:


1.0.163+g45474d37ed

Usage: libman [options] [command]

Options:
--help|-h Show help information
--version Show version information

Commands:
cache List or clean libman cache contents
clean Deletes all library files defined in libman.json from the project
init Create a new libman.json
install Add a library definition to the libman.json file, and download the
library to the specified location
restore Downloads all files from provider and saves them to specified
destination
uninstall Deletes all files for the specified library from their specified
destination, then removes the specified library definition from
libman.json
update Updates the specified library

Use "libman [command] --help" for more information about a command.

Las siguientes secciones describen los comandos CLI disponibles.

Inicializar LibMan en el proyecto


El libman init comando crea un libman.json archivo si no existe ninguno. El archivo se crea con el contenido
predeterminado de la plantilla de elemento.
Sinopsis

libman init [-d|--default-destination] [-p|--default-provider] [--verbosity]


libman init [-h|--help]

Opciones
Las siguientes opciones están disponibles para el libman init comando:
-d|--default-destination <PATH>

Una ruta de acceso relativa a la carpeta actual. Archivos de biblioteca se instalan en esta ubicación si no hay
ningún destination propiedad está definida para una biblioteca en libman.json. El <PATH> valor se escribe
en el defaultDestination propiedad de libman.json.
-p|--default-provider <PROVIDER>

El proveedor debe usar si no se define ningún proveedor para una determinada biblioteca. El <PROVIDER>
valor se escribe en el defaultProvider propiedad de libman.json. Reemplace <PROVIDER> con uno de los
siguientes valores:
cdnjs
filesystem
unpkg

-h|--help

Mostrar información de ayuda.


--verbosity <LEVEL>
Establecer el nivel de detalle de la salida. Reemplace <LEVEL> con uno de los siguientes valores:
quiet
normal
detailed

Ejemplos
Para crear un libman.json archivo en un proyecto de ASP.NET Core:
Vaya a la raíz del proyecto.
Ejecute el siguiente comando:

libman init

Escriba el nombre del proveedor predeterminado, o presione Enter para utilizar el proveedor CDNJS
predeterminado. Los valores válidos son:
cdnjs
filesystem
unpkg

Un libman.json archivo se agrega a la raíz del proyecto con el siguiente contenido:

{
"version": "1.0",
"defaultProvider": "cdnjs",
"libraries": []
}

Agregar archivos de biblioteca


El libman install comando descarga e instala los archivos de biblioteca en el proyecto. Un libman.json se agrega
un archivo si no existe. El libman.json se ha modificado para almacenar los detalles de configuración para los
archivos de biblioteca.
Sinopsis

libman install <LIBRARY> [-d|--destination] [--files] [-p|--provider] [--verbosity]


libman install [-h|--help]

Argumentos
LIBRARY

El nombre de la biblioteca para instalar. Este nombre puede incluir una notación de número de versión (por
ejemplo, @1.2.0 ).
Opciones
Las siguientes opciones están disponibles para el libman install comando:
-d|--destination <PATH>

La ubicación para instalar la biblioteca. Si no se especifica, se usa la ubicación predeterminada. Si no hay


ningún defaultDestination propiedad se especifica en libman.json, esta opción es necesaria.
--files <FILE>

Especifique el nombre del archivo que se instale desde la biblioteca. Si no se especifica, se instalan todos los
archivos de la biblioteca. Proporcione uno --files opción por archivo para instalarse. También se admiten
rutas de acceso relativas. Por ejemplo: --files dist/browser/signalr.js .
-p|--provider <PROVIDER>

El nombre del proveedor que se usará para la adquisición de la biblioteca. Reemplace <PROVIDER> con uno
de los siguientes valores:
cdnjs
filesystem
unpkg

Si no se especifica, el defaultProvider propiedad libman.json se utiliza. Si no hay ningún defaultProvider


propiedad se especifica en libman.json, esta opción es necesaria.
-h|--help

Mostrar información de ayuda.


--verbosity <LEVEL>

Establecer el nivel de detalle de la salida. Reemplace <LEVEL> con uno de los siguientes valores:
quiet
normal
detailed

Ejemplos
Tenga en cuenta la siguiente libman.json archivo:

{
"version": "1.0",
"defaultProvider": "cdnjs",
"libraries": []
}

Para instalar la versión de jQuery 3.2.1 jquery.min.js del archivo a la wwwroot/scripts/jquery carpeta mediante el
proveedor CDNJS:

libman install jquery@3.2.1 --provider cdnjs --destination wwwroot/scripts/jquery --files jquery.min.js

El libman.json archivo es similar al siguiente:


{
"version": "1.0",
"defaultProvider": "cdnjs",
"libraries": [
{
"library": "jquery@3.2.1",
"destination": "wwwroot/scripts/jquery",
"files": [
"jquery.min.js"
]
}
]
}

Para instalar el calendar.js y calendar.css archivos desde C:\temp\contosoCalendar\ con el sistema de archivos
proveedor:

libman install C:\temp\contosoCalendar\ --provider filesystem --files calendar.js --files calendar.css

Aparece el mensaje siguiente por dos motivos:


El libman.json archivo no contiene un defaultDestination propiedad.
El libman install comando no debe contener el -d|--destination opción.

Después de aceptar el destino predeterminado, el libman.json archivo es similar al siguiente:


{
"version": "1.0",
"defaultProvider": "cdnjs",
"libraries": [
{
"library": "jquery@3.2.1",
"destination": "wwwroot/scripts/jquery",
"files": [
"jquery.min.js"
]
},
{
"library": "C:\\temp\\contosoCalendar\\",
"provider": "filesystem",
"destination": "wwwroot/lib/contosoCalendar",
"files": [
"calendar.js",
"calendar.css"
]
}
]
}

Restaurar archivos de biblioteca


El libman restore comando instala los archivos de biblioteca definidos en libman.json. Se aplican las siguientes
reglas:
Si no hay ningún libman.json archivo existe en la raíz del proyecto, se devuelve un error.
Si una biblioteca especifica un proveedor, el defaultProvider propiedad libman.json se omite.
Si una biblioteca especifica un destino, el defaultDestination propiedad libman.json se omite.
Sinopsis

libman restore [--verbosity]


libman restore [-h|--help]

Opciones
Las siguientes opciones están disponibles para el libman restore comando:
-h|--help

Mostrar información de ayuda.


--verbosity <LEVEL>

Establecer el nivel de detalle de la salida. Reemplace <LEVEL> con uno de los siguientes valores:
quiet
normal
detailed

Ejemplos
Para restaurar los archivos de biblioteca definidos en libman.json:

libman restore
Eliminar archivos de biblioteca
El libman clean comando elimina los archivos de biblioteca previamente se restaurará a través de LibMan. Se
eliminan las carpetas que se convierten en vacías después de esta operación. Los archivos de biblioteca asociados
a las configuraciones en el libraries propiedad de libman.json no se quitan.
Sinopsis

libman clean [--verbosity]


libman clean [-h|--help]

Opciones
Las siguientes opciones están disponibles para el libman clean comando:
-h|--help

Mostrar información de ayuda.


--verbosity <LEVEL>

Establecer el nivel de detalle de la salida. Reemplace <LEVEL> con uno de los siguientes valores:
quiet
normal
detailed

Ejemplos
Para eliminar los archivos de biblioteca instalados a través de LibMan:

libman clean

Desinstalar los archivos de biblioteca


El libman uninstall comando:
Elimina todos los archivos asociados con la biblioteca especificada desde el destino en libman.json.
Quita la configuración de biblioteca asociado desde libman.json.
Se produce un error cuando:
No libman.json archivo existe en la raíz del proyecto.
La biblioteca especificada no existe.
Si se instala más de una biblioteca con el mismo nombre, se le pedirá que elija uno.
Sinopsis

libman uninstall <LIBRARY> [--verbosity]


libman uninstall [-h|--help]

Argumentos
LIBRARY

El nombre de la biblioteca que se va a desinstalar. Este nombre puede incluir una notación de número de versión
(por ejemplo, @1.2.0 ).
Opciones
Las siguientes opciones están disponibles para el libman uninstall comando:
-h|--help

Mostrar información de ayuda.


--verbosity <LEVEL>

Establecer el nivel de detalle de la salida. Reemplace <LEVEL> con uno de los siguientes valores:
quiet
normal
detailed

Ejemplos
Tenga en cuenta la siguiente libman.json archivo:

{
"version": "1.0",
"defaultProvider": "cdnjs",
"libraries": [
{
"library": "jquery@3.3.1",
"files": [
"jquery.min.js",
"jquery.js",
"jquery.min.map"
],
"destination": "wwwroot/lib/jquery/"
},
{
"provider": "unpkg",
"library": "bootstrap@4.1.3",
"destination": "wwwroot/lib/bootstrap/"
},
{
"provider": "filesystem",
"library": "C:\\temp\\lodash\\",
"files": [
"lodash.js",
"lodash.min.js"
],
"destination": "wwwroot/lib/lodash/"
}
]
}

Para desinstalar jQuery, de los siguientes comandos se completan correctamente:

libman uninstall jquery

libman uninstall jquery@3.3.1

Para desinstalar los archivos de Lodash instalados a través de la filesystem proveedor:

libman uninstall C:\temp\lodash\


Actualizar la versión de la biblioteca
El libman update comando actualiza una biblioteca se instala a través de LibMan a la versión especificada.
Se produce un error cuando:
No libman.json archivo existe en la raíz del proyecto.
La biblioteca especificada no existe.
Si se instala más de una biblioteca con el mismo nombre, se le pedirá que elija uno.
Sinopsis

libman update <LIBRARY> [-pre] [--to] [--verbosity]


libman update [-h|--help]

Argumentos
LIBRARY

El nombre de la biblioteca para actualizar.


Opciones
Las siguientes opciones están disponibles para el libman update comando:
-pre

Obtener la última versión preliminar de la biblioteca.


--to <VERSION>

Obtener una versión específica de la biblioteca.


-h|--help

Mostrar información de ayuda.


--verbosity <LEVEL>

Establecer el nivel de detalle de la salida. Reemplace <LEVEL> con uno de los siguientes valores:
quiet
normal
detailed

Ejemplos
Para actualizar jQuery para la versión más reciente:

libman update jquery

Para actualizar jQuery para la versión 3.3.1:

libman update jquery --to 3.3.1

Para actualizar jQuery para la versión preliminar más reciente:

libman update jquery -pre


Administrar la caché de la biblioteca
El libman cache comando administra la caché de la biblioteca LibMan. El filesystem proveedor no usa la caché
de la biblioteca.
Sinopsis

libman cache clean [<PROVIDER>] [--verbosity]


libman cache list [--files] [--libraries] [--verbosity]
libman cache [-h|--help]

Argumentos
PROVIDER

Solo se usa con el clean comando. Especifica la caché del proveedor para limpiar. Los valores válidos son:
cdnjs
filesystem
unpkg

Opciones
Las siguientes opciones están disponibles para el libman cache comando:
--files

Enumerar los nombres de los archivos que se almacenan en caché.


--libraries

Enumerar los nombres de las bibliotecas que se almacenan en caché.


-h|--help

Mostrar información de ayuda.


--verbosity <LEVEL>

Establecer el nivel de detalle de la salida. Reemplace <LEVEL> con uno de los siguientes valores:
quiet
normal
detailed

Ejemplos
Para ver los nombres de las bibliotecas en caché por el proveedor, use uno de los siguientes comandos:

libman cache list

libman cache list --libraries

Esto genera una salida similar a la siguiente:


Cache contents:
---------------
unpkg:
knockout
react
vue
cdnjs:
font-awesome
jquery
knockout
lodash.js
react

Para ver los nombres de archivos de biblioteca almacenados en caché por el proveedor:

libman cache list --files

Esto genera una salida similar a la siguiente:

Cache contents:
---------------
unpkg:
knockout:
<list omitted for brevity>
react:
<list omitted for brevity>
vue:
<list omitted for brevity>
cdnjs:
font-awesome
metadata.json
jquery
metadata.json
3.2.1\core.js
3.2.1\jquery.js
3.2.1\jquery.min.js
3.2.1\jquery.min.map
3.2.1\jquery.slim.js
3.2.1\jquery.slim.min.js
3.2.1\jquery.slim.min.map
3.3.1\core.js
3.3.1\jquery.js
3.3.1\jquery.min.js
3.3.1\jquery.min.map
3.3.1\jquery.slim.js
3.3.1\jquery.slim.min.js
3.3.1\jquery.slim.min.map
knockout
metadata.json
3.4.2\knockout-debug.js
3.4.2\knockout-min.js
lodash.js
metadata.json
4.17.10\lodash.js
4.17.10\lodash.min.js
react
metadata.json

Observe que la salida anterior muestra que jQuery se almacenan en caché las versiones 3.2.1 y 3.3.1 con el
proveedor de CDNJS.
Para vaciar la caché de biblioteca para el proveedor CDNJS:
libman cache clean cdnjs

Después de vaciar la caché del proveedor CDNJS, el libman cache list comando muestra lo siguiente:

Cache contents:
---------------
unpkg:
knockout
react
vue
cdnjs:
(empty)

Para vaciar la memoria caché para todos los proveedores admitidos:

libman cache clean

Después de vaciar todas las cachés de proveedor, el libman cache list comando muestra lo siguiente:

Cache contents:
---------------
unpkg:
(empty)
cdnjs:
(empty)

Recursos adicionales
Instale una herramienta Global
Usar LibMan con ASP.NET Core en Visual Studio
Repositorio de LibMan en GitHub
Usar LibMan con ASP.NET Core en Visual Studio
31/08/2018 • 16 minutes to read • Edit Online

Por Scott Addie


Visual Studio tiene compatibilidad integrada para LibMan en proyectos de ASP.NET Core, incluidos:
Soporte técnico para configurar y ejecutar operaciones de restauración LibMan en la compilación.
Elementos de menú para desencadenar la restauración LibMan y las operaciones de limpieza.
Cuadro de diálogo de búsqueda para buscar bibliotecas y agregar los archivos a un proyecto.
Compatibilidad con para la edición libman.json—el archivo de manifiesto LibMan.
Ver o descargar el código de ejemplo (cómo descargar)

Requisitos previos
Visual Studio 2017 versión 15,8 o posterior con el ASP.NET y desarrollo web carga de trabajo

Agregar archivos de biblioteca


Archivos de biblioteca pueden agregarse a un proyecto de ASP.NET Core de dos maneras diferentes:
1. Utilice el cuadro de diálogo Agregar biblioteca de cliente
2. Configurar manualmente las entradas del archivo de manifiesto de LibMan
Utilice el cuadro de diálogo Agregar biblioteca de cliente
Siga estos pasos para instalar una biblioteca de cliente:
En el Explorador de soluciones, haga clic en la carpeta del proyecto en el que se deben agregar los
archivos. Elija agregar > biblioteca del cliente. El Agregar biblioteca de cliente aparece el cuadro de
diálogo:

Seleccione el proveedor de la biblioteca desde el proveedor lista desplegable. CDNJS es el proveedor


predeterminado.
Escriba el nombre de la biblioteca para capturar en el biblioteca cuadro de texto. IntelliSense proporciona
una lista de bibliotecas, empezando por el texto proporcionado.
Seleccione la biblioteca de la lista de IntelliSense. Tenga en cuenta el nombre de la biblioteca tiene el sufijo
del @ símbolos y la versión estable más reciente que se sabe que el proveedor seleccionado.
Decida qué archivos se incluyen:
Seleccione el incluir todos los archivos de biblioteca botón de opción para incluir todos los archivos
de la biblioteca.
Seleccione el elegir archivos específicos botón de opción para incluir un subconjunto de archivos de
la biblioteca. Cuando se selecciona el botón de radio, se habilita el árbol del selector de archivos. Active
las casillas a la izquierda de los nombres de archivo para descargar.
Especifique la carpeta del proyecto para almacenar los archivos en el ubicación de destino cuadro de
texto. Como recomendación, almacenar cada biblioteca en una carpeta independiente.
El texto sugerido ubicación de destino carpeta se basa en la ubicación desde donde se inició el cuadro de
diálogo:
Si se inicia desde la raíz del proyecto:
Wwwroot/lib se utiliza si wwwroot existe.
lib se utiliza si wwwroot no existe.
Si se inicia desde una carpeta de proyecto, se usa el nombre de la carpeta correspondiente.
La sugerencia de la carpeta tiene el sufijo del nombre de la biblioteca. La siguiente tabla muestra las
sugerencias de carpeta al instalar jQuery en un proyecto de páginas de Razor.

UBICACIÓN DE INICIO CARPETA SUGERIDO

raíz del proyecto (si wwwroot existe) Wwwroot/lib/jquery /

raíz del proyecto (si wwwroot no existe) jquery/lib /

Páginas carpeta del proyecto Páginas/jquery /

Haga clic en el instalar botón para descargar los archivos, por la configuración en libman.json.
Revise el Administrador de bibliotecas fuente de la salida ventana de detalles de la instalación. Por
ejemplo:

Restore operation started...


Restoring libraries for project LibManSample
Restoring library jquery@3.3.1... (LibManSample)
wwwroot/lib/jquery/jquery.min.js written to destination (LibManSample)
wwwroot/lib/jquery/jquery.js written to destination (LibManSample)
wwwroot/lib/jquery/jquery.min.map written to destination (LibManSample)
Restore operation completed
1 libraries restored in 2.32 seconds

Configurar manualmente las entradas del archivo de manifiesto de LibMan


Todas las operaciones de LibMan en Visual Studio se basan en el contenido del manifiesto de la raíz proyecto
LibMan (libman.json). Puede editar manualmente libman.json para configurar los archivos de biblioteca para el
proyecto. Visual Studio restaura todos los archivos de biblioteca una vez libman.json se guarda.
Para abrir libman.json para su edición, existen las siguientes opciones:
Haga doble clic en el libman.json archivo el Explorador de soluciones.
Haga clic en el proyecto en el Explorador de soluciones y seleccione administrar bibliotecas de cliente. †
Seleccione administrar bibliotecas de cliente desde Visual Studio proyecto menú. †
† Si el libman.json archivo ya no existe en la raíz del proyecto, se creará con el contenido predeterminado de la
plantilla de elemento.
Visual Studio ofrece JSON enriquecido compatibilidad como la coloración de edición, formato, IntelliSense y
validación del esquema. Esquema JSON del manifiesto LibMan se encuentra en
http://json.schemastore.org/libman .
Con el archivo de manifiesto siguiente, LibMan recupera archivos por la configuración definida en el libraries
propiedad. Obtener una explicación de los literales de objeto definidos dentro de libraries sigue:
Un subconjunto de jQuery versión 3.3.1 se recupera desde el proveedor CDNJS. El subconjunto se define en el
files propiedad—jquery.min.js, archivo jquery.js, y jquery.min.map. Los archivos se colocan en el proyecto
wwwroot/lib/jquery carpeta.
La totalidad de Bootstrap versión 4.1.3 se recuperan y se coloca en un wwwroot/lib/bootstrap carpeta. El literal
de objeto provider reemplazos de propiedad el defaultProvider valor de propiedad. LibMan recupera los
archivos de arranque del proveedor unpkg.
Un subconjunto de Lodash fue aprobada por un organismo dentro de la organización. El lodash.js y
lodash.min.js se recuperan archivos de sistema de archivos local en C:\temp\lodash\. Los archivos se copian en
el proyecto wwwroot/lib/lodash carpeta.

{
"version": "1.0",
"defaultProvider": "cdnjs",
"libraries": [
{
"library": "jquery@3.3.1",
"files": [
"jquery.min.js",
"jquery.js",
"jquery.min.map"
],
"destination": "wwwroot/lib/jquery/"
},
{
"provider": "unpkg",
"library": "bootstrap@4.1.3",
"destination": "wwwroot/lib/bootstrap/"
},
{
"provider": "filesystem",
"library": "C:\\temp\\lodash\\",
"files": [
"lodash.js",
"lodash.min.js"
],
"destination": "wwwroot/lib/lodash/"
}
]
}

NOTE
LibMan sólo admite una versión de cada biblioteca de cada proveedor. El libman.json archivo no supera la validación de
esquema si contiene dos bibliotecas con el mismo nombre de biblioteca para un proveedor determinado.

Restaurar archivos de biblioteca


Para restaurar archivos de biblioteca desde Visual Studio, debe haber un válido libman.json archivo en la raíz del
proyecto. Archivos restaurados se colocan en el proyecto en la ubicación especificada para cada biblioteca.
Archivos de biblioteca se pueden restaurar en un proyecto de ASP.NET Core de dos maneras:
1. Restaurar archivos durante la compilación
2. Restaurar los archivos manualmente
Restaurar archivos durante la compilación
LibMan puede restaurar los archivos de biblioteca definidos como parte del proceso de compilación. De forma
predeterminada, el restauración en compilación comportamiento está deshabilitado.
Para habilitar y probar el comportamiento de compilación de restauración:
Haga clic en libman.json en el Explorador de soluciones y seleccione habilitar la restauración de las
bibliotecas de cliente en la compilación en el menú contextual.
Haga clic en el Sí botón cuando se le pida que instale un paquete de NuGet. El
Microsoft.Web.LibraryManager.Build paquete NuGet se agrega al proyecto:

<PackageReference Include="Microsoft.Web.LibraryManager.Build" Version="1.0.113" />

Compile el proyecto para confirmar que se produce la restauración de archivos LibMan. El


Microsoft.Web.LibraryManager.Build paquete inyecta un destino de MSBuild que se ejecuta LibMan durante
la operación de compilación del proyecto.
Revise el compilar fuente de la salida ventana para un registro de actividad LibMan:

1>------ Build started: Project: LibManSample, Configuration: Debug Any CPU ------
1>
1>Restore operation started...
1>Restoring library jquery@3.3.1...
1>Restoring library bootstrap@4.1.3...
1>
1>2 libraries restored in 10.66 seconds
1>LibManSample -> C:\LibManSample\bin\Debug\netcoreapp2.1\LibManSample.dll
========== Build: 1 succeeded, 0 failed, 0 up-to-date, 0 skipped ==========

Cuando se habilita el comportamiento de compilación de restauración, el libman.json menú contextual muestra un


deshabilitar Restaurar las bibliotecas de cliente en la compilación opción. Al seleccionar esta opción quita el
Microsoft.Web.LibraryManager.Build referencia del archivo de proyecto del paquete. Por lo tanto, las bibliotecas de
cliente ya no se restauran en cada compilación.
Independientemente de la configuración de compilación de restauración, puede restaurar manualmente en
cualquier momento desde el libman.json menú contextual. Para obtener más información, consulte restaurar
manualmente los archivos.
Restaurar los archivos manualmente
Para restaurar manualmente los archivos de biblioteca:
Todos los proyectos de la solución:
Haga clic en el nombre de la solución en el Explorador de soluciones.
Seleccione el las bibliotecas de cliente de restauración opción.
Para un proyecto específico:
Haga clic en el libman.json archivo el Explorador de soluciones.
Seleccione el las bibliotecas de cliente de restauración opción.
Mientras se está ejecutando la operación de restauración:
El icono del centro de estado de tarea (TSC ) en la barra de estado de Visual Studio se animará y leerá inició
la operación de restauración. Al hacer clic en el icono se abre una lista de las tareas de fondo conocidos de
información sobre herramientas.
Los mensajes se enviarán a la barra de estado y el Administrador de bibliotecas fuente de la salida
ventana. Por ejemplo:

Restore operation started...


Restoring libraries for project LibManSample
Restoring library jquery@3.3.1... (LibManSample)
wwwroot/lib/jquery/jquery.min.js written to destination (LibManSample)
wwwroot/lib/jquery/jquery.js written to destination (LibManSample)
wwwroot/lib/jquery/jquery.min.map written to destination (LibManSample)
Restore operation completed
1 libraries restored in 2.32 seconds

Eliminar archivos de biblioteca


Para realizar la limpia operación, lo que elimina los archivos de biblioteca restaurados anteriormente en Visual
Studio:
Haga clic en el libman.json archivo el Explorador de soluciones.
Seleccione el limpia de las bibliotecas de cliente opción.
Para evitar la eliminación accidental de archivos de la biblioteca no, la operación de limpieza no elimina directorios
enteros. Solo quita los archivos que se incluyeron en la restauración anterior.
Mientras se está ejecutando la operación de limpieza:
En la barra de estado de Visual Studio en el icono TSC se animará y leerá inició la operación de las bibliotecas
de cliente. Al hacer clic en el icono se abre una lista de las tareas de fondo conocidos de información sobre
herramientas.
Los mensajes se envían a la barra de estado y el Administrador de bibliotecas fuente de la salida ventana.
Por ejemplo:

Clean libraries operation started...


Clean libraries operation completed
2 libraries were successfully deleted in 1.91 secs

La operación de limpieza elimina solo los archivos del proyecto. Archivos de biblioteca permanecen en la memoria
caché para una recuperación más rápida en las operaciones de restauración futuras. Para administrar archivos de
biblioteca almacenados en memoria caché del equipo local, use el LibMan CLI.

Desinstalar los archivos de biblioteca


Para desinstalar los archivos de biblioteca:
Abra libman.json.
Colocar el símbolo de intercalación dentro de la correspondiente libraries literal de objeto.
Haga clic en el icono de bombilla que aparece en el margen izquierdo y seleccione desinstalar
<nombre_de_biblioteca > @<library_version >:
Como alternativa, puede editar y guardar el manifiesto LibMan manualmente (libman.json). El la operación de
restauración se ejecuta cuando se guarda el archivo. Archivos de biblioteca que ya no se definen en libman.json se
quitó del proyecto.

Actualizar la versión de la biblioteca


Para comprobar si una versión actualizada de biblioteca:
Abra libman.json.
Colocar el símbolo de intercalación dentro de la correspondiente libraries literal de objeto.
Haga clic en el icono de bombilla que aparece en el margen izquierdo. Mantenga el mouse sobre buscar
actualizaciones.
LibMan comprueba si hay una versión más reciente que la versión instalada de la biblioteca. Pueden producirse
los siguientes resultados:
Un No se encontraron actualizaciones se muestra un mensaje si ya está instalada la versión más
reciente.
La versión estable más reciente se muestra si no está instalada.

Si está disponible una versión preliminar más reciente que la versión instalada, se muestra la versión
preliminar.
Para degradar a una versión anterior de la biblioteca, edite manualmente el libman.json archivo. Cuando se
guarda el archivo, el LibMan la operación de restauración:
Quita archivos redundantes de la versión anterior.
Agrega los archivos nuevos y actualizados de la nueva versión.

Recursos adicionales
Usar la interfaz de línea de comandos (CLI) de LibMan con ASP.NET Core
Repositorio de LibMan en GitHub
Administrar los paquetes del lado cliente con Bower
en ASP.NET Core
23/08/2018 • 10 minutes to read • Edit Online

Por Rick Anderson, Noel arroz, y Scott Addie

IMPORTANT
Mientras se mantiene Bower, sus maintainers recomienda usar una solución diferente. Administrador de bibliotecas (LibMan
para abreviar) es la herramienta de adquisición de biblioteca de cliente nuevo de Visual Studio (Visual Studio 15,8 o
posterior). Para obtener más información, consulta Adquisición de bibliotecas del lado cliente en ASP.NET Core con LibMan.
Bower es compatible con Visual Studio a través de la versión 15.5.
Yarn con Webpack es una alternativa popular para el que instrucciones de migración están disponibles.

Bower llama a sí mismo "Administrador de paquetes para la web". En el ecosistema de. NET, llena este vacío a la
incapacidad de NuGet para entregar archivos de contenido estático a la izquierda. Para los proyectos de ASP.NET
Core, estos archivos estáticos son inherentes a las bibliotecas de cliente como jQuery y Bootstrap. Para las
bibliotecas. NET, seguir usando NuGet Administrador de paquetes.
Proceso de compilación de nuevos proyectos creados con las plantillas de proyecto de ASP.NET Core que
configurar el cliente. jQuery y Bootstrap están instalados, y es compatible con Bower.
Los paquetes del lado cliente se muestran en el bower.json archivo. Las plantillas de proyecto de ASP.NET Core
configura bower.json con jQuery, validación de jQuery y Bootstrap.
En este tutorial, vamos a agregar compatibilidad para Font Awesome. Los paquetes de bower pueden instalarse
con la administrar paquetes de Bower la interfaz de usuario o manualmente en el bower.json archivo.
Instalación a través de paquetes de Bower de administrar la interfaz de usuario
Crear una nueva aplicación Web de ASP.NET Core con la 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> , haga clic en la pestaña "Examinar" y, a continuación, filtre la lista
de paquetes escribiendo font-awesome en el cuadro de búsqueda:
Confirme que el "guardar los cambios en bower.json" está activada la casilla de verificación. 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 el bower.json archivo y agregue "font awesome" a las dependencias. IntelliSense muestra los paquetes
disponibles. Cuando se selecciona un paquete, se muestran las versiones disponibles. Las imágenes siguientes
sean más antiguas y no coincidirán con lo que ve.

Usos de bower versionamiento 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 mostrando solo algunas opciones
comunes. El elemento superior de la lista de IntelliSense (4.6.3 en el ejemplo anterior) se considera la última
versión estable del paquete. El símbolo de intercalación (^) coincide con la versión principal más reciente y la tilde
() con la versión secundaria más reciente.
Guardar el bower.json archivo. Visual Studio inspecciona el bower.json archivo para los cambios. Al guardar, el
bower install se ejecuta el comando. Vea la ventana de salida Bower o npm vista para el comando exacto que se
ejecuta.
Abra el bowerrc archivo bower.json. El directory propiedad está establecida en wwwroot/lib que indica la
ubicación Bower instalará los activos del paquete.

{
"directory": "wwwroot/lib"
}

Puede usar el cuadro de búsqueda en el Explorador de soluciones para buscar y mostrar el paquete font awesome.
Abra el Views\Shared_Layout.cshtml y agréguele el archivo CSS font awesome el entorno aplicación auxiliar de
etiquetas 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 por 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>

Ejecute la aplicación y vaya a la vista About para comprobar que funciona la impresionante de fuente del paquete.

Explorar el proceso de compilación del lado cliente


La mayoría de las plantillas de proyecto de ASP.NET Core ya están configuradas para usar Bower. En este tutorial
siguiente se inicia 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 tal como se realiza cada cambio de configuración de salida.
Los pasos generales para usar el proceso de compilación del lado cliente con Bower son:
Definir los paquetes utilizados en el proyecto.
Paquetes de referencia de las páginas web.
Definir paquetes
Una vez que la lista de paquetes en el bower.json archivo, Visual Studio, descargará. En el ejemplo siguiente se usa
Bower para cargar jQuery y Bootstrap para el wwwroot carpeta.
Crear una nueva aplicación Web de ASP.NET Core con la 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.jsonasí como agregar jquery y bootstrap para 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 el bootstrap y jQuery directorios en wwwroot/lib. Bower usa el bowerrc
archivo para instalar los activos de 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 al proyecto.
Habilitar archivos estáticos atender las solicitudes 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.
Agregue una nueva página HTML denominada Index.html a la wwwroot carpeta. Nota: Debe agregar el
archivo HTML para el wwwroot carpeta. De forma predeterminada, no se pueden atender contenido
estático fuera wwwroot. Consulte 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 . Como alternativa, con Index.html abierto,


presione Ctrl+Shift+W . Compruebe que se aplica el estilo jumbotron, el código jQuery responde cuando se
hace clic en el botón y que el botón Bootstrap cambia de estado.
Crear sitios atractivos y capacidad de respuesta con
Bootstrap y ASP.NET Core
23/08/2018 • 23 minutes to read • Edit Online

Por Steve Smith


Bootstrap es actualmente el marco 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 del usuario con el sitio
web, si tiene experiencia en desarrollo y diseño front-end o a 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 escalar
eficazmente desde teléfonos a tabletas hasta equipos de escritorio.

Primeros pasos
Hay varias maneras de empezar a trabajar con Bootstrap. Si está iniciando una nueva aplicación web en Visual
Studio, puede elegir la plantilla de inicio predeterminado para ASP.NET Core, en la que procederá preinstalado
Bootstrap case:

Adición de Bootstrap a un núcleo de ASP.NET proyecto es simplemente una 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 utilizando uno de varios administradores de paquetes, como NuGet, npm o Bower de
bootstrap. 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 Bootstrap en ASP.NET Core es a través de
Bower (mediante bower.json, tal y como se muestra arriba). Se muestran el uso de npm y NuGet para demostrar cómo
Bootstrap puede agregarse fácilmente a otros tipos de aplicaciones web, incluidas las versiones anteriores de ASP.NET.

Si está haciendo referencia a sus propias versiones locales de Bootstrap, deberá hacer referencia a ellos en las
páginas que va a usar. En producción debe hacer referencia mediante una red CDN de bootstrap. En la plantilla de
sitio ASP.NET Core predeterminada, el _Layout.cshtml archivo así como este:

<!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 va a usar cualquiera de los complementos de jQuery de Bootstrap, también deberá hacer referencia a jQuery.

Las características y las plantillas básicas


La plantilla más básica de Bootstrap se parece mucho a la _Layout.cshtml archivo que se muestra arriba y
simplemente incluye un menú básico para la navegación y un lugar para representar el resto de la página.
Navegación básica
La plantilla predeterminada usa un conjunto de <div> elementos para representar una barra de navegación
superior y el cuerpo de la página principal. Si usa HTML5, puede reemplazar la primera <div> etiquetar con un
<nav> etiqueta para obtener el mismo efecto, pero con una semántica más precisa. En este primer <div> puede
ver que hay otras. En primer lugar, un <div> con una clase de "contenedor" y, a continuación, en que, más dos
<div> elementos: "navbar -header" y "navbar -collapse". La etiqueta 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 llamado "icono de la hamburguesa"). El icono se representa utilizando puro
HTML y CSS; no se requiere ninguna imagen. Este es el código que muestra el icono, con cada uno de los
etiquetas representar una de las barras en blanco:
<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 mediante el <ul> elemento dentro de la segunda etiqueta 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 otro <div> , marcados con las clases de "contenedor" y "contenido del cuerpo". En el valor
predeterminado simple _archivo de diseño se muestra a continuación, el contenido de la página se representa por
la vista concreta asociada con la página y, a continuación, un simple <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 "hamburguesa" en la esquina superior derecha, aparece cuando la
ventana cae por debajo de un determinado ancho:
Al hacer clic en el icono se revela los elementos de menú en un cajón vertical que se desliza hacia abajo desde la
parte superior de la página:

Vínculos y tipografía
Bootstrap configura el sitio tipografía básica, colores y el formato en su archivo CSS del enlace. Este archivo CSS
incluye los estilos predeterminados para las tablas, botones, elementos de formulario, imágenes etc. (más). Una
característica particularmente útil es el sistema de diseño de cuadrícula, que explicaré a continuación.
Cuadrículas
Una de las características más populares de Bootstrap es su sistema de diseño de cuadrícula. Aplicaciones web
modernas deben evitar el uso del <table> etiqueta para el diseño, en su lugar, restringir el uso de este elemento
con los 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 de este enfoque, incluida la capacidad de ajustar el
diseño de cuadrículas para mostrar verticalmente en las pantallas estrechas, como en teléfonos.
Sistema de diseño de cuadrícula de bootstrap se basa en doce columnas. Este número se ha elegido porque
pueden dividirse uniformemente en 1, 2, 3 o 4 columnas, y los anchos de columna pueden variar para 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 <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, agregue 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 tener dos columnas del mismo tamaño, podría usar una clase de "col-md-6" para cada uno de ellos.
En este caso "md" es la abreviatura de "medium" y hace referencia a los 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
anchos de mayor a menos que se reemplaza (por lo que si desea que el diseño corregirse, independientemente
del ancho de pantalla, basta con especificar clases xs).

PREFIJO DE LA CLASE CSS CAPA 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 con resoluciones de escritorio,
pero estas dos columnas se apilarán verticalmente cuando se representa en dispositivos más pequeños (o una
ventana del explorador estrecha en un equipo de escritorio), lo que permite a los usuarios ver fácilmente
contenido sin necesidad de desplazarse horizontalmente.
Bootstrap siempre será un diseño de una sola columna, por lo que solo deberá especificar las 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, deberá restablecer la representación de columnas en determinados puntos. Agregar un
elemento div clearfix que solo es visible dentro de una ventanilla de determinados puede lograr esto, como se
muestra aquí:
En el ejemplo anterior, uno y dos comparten una fila en el diseño "md", mientras 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 "xs" (tenga en cuenta
que se muestra solo uno, cuatro y cinco):
En este ejemplo, una única fila <div> se ha usado, y sigue Bootstrap principalmente hicieron lo correcto en
relación con el diseño y el agrupamiento de las columnas. Normalmente, debe especificar una fila <div> para
cada fila horizontal requiere el diseño y, por supuesto puede anidar las cuadrículas de Bootstrap dentro de uno a
otro. Al hacerlo, cada cuadrícula anidada ocupará el 100% del ancho del elemento en el que se coloca, que, a
continuación, se puede subdividir mediante las clases de columna.
Jumbotron
Si ha usado las plantillas de ASP.NET Core MVC predeterminada en Visual Studio 2012 o 2013, probablemente
ha visto la Jumbotron en acción. Se refiere a una sección grande de ancho completo de una página que se puede
usar para mostrar una imagen de fondo de gran tamaño, una llamada a la acción, una rotación o elementos
similares. Para agregar un jumbotron a una página, agregue simplemente un <div> y asígnele una clase de
"jumbotron", a continuación, coloque un contenedor <div> dentro y agregar contenido. Podemos ajustar
fácilmente el estándar acerca de la página que se usará un jumbotron para los encabezados principales que
muestra:

Botones
En la ilustración siguiente se muestran las clases del botón predeterminado y sus colores.
Distintivos
Distintivos hacen referencia a pequeño, normalmente numéricas llamadas junto a un elemento de navegación.
Puede indicar un número de mensajes o notificaciones en espera, o la presencia de las actualizaciones. Especificar
dichas notificaciones es tan sencillo como agregar un <span> que contiene el texto, con una clase de
"notificación":

Alertas
Es posible que deba mostrar algún tipo de notificación, alerta o mensaje de error a los usuarios de la aplicación. Es
donde las clases estándar de alerta son útiles. Hay cuatro niveles de gravedad diferentes con esquemas de color
asociado:
Menús y barras de exploración
Nuestro diseño ya incluye una barra de navegación estándar, pero el tema de Bootstrap admite opciones
adicionales de la aplicación de estilos. También podemos optar para mostrar la barra de navegación vertical, en
lugar de horizontalmente si lo que se prefiere, así como agregar subnavegación los elementos de menús
desplegables de. Menús de navegación sencillas, como bandas de pestaña, se crean en la parte superior de <ul>
elementos. Éstos se pueden crear muy basta con que se proporcionen con las clases CSS "nav" y "pestañas de
navegación":
Barras de exploración se compilan de forma similar, pero son un poco más complejos. Se inician con un <nav> o
<div> con una clase de "barra de navegación," dentro del cual un div contenedor contiene el resto de los
elementos. Nuestra página incluye una barra de navegación en su encabezado ya: se muestra a continuación
simplemente se expande en este caso, agregar compatibilidad con un menú desplegable:
Elementos adicionales
También puede utilizarse el tema predeterminado para presentar las tablas HTML en un estilo bien formateado,
incluida la compatibilidad con vistas seccionadas. Existen etiquetas con los estilos que son similares a las de los
botones. Puede crear menús desplegables personalizados que admiten opciones de estilos adicionales más allá
del 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, existen varios estilos que elegir, así como
enumerar los grupos y paneles que incluyen un título y el contenido. Explorar opciones adicionales del tema de
Bootstrap estándar aquí:
http://getbootstrap.com/examples/theme/
Más temas
Puede ampliar el tema de Bootstrap estándar mediante la invalidación de 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 de tema disponibles en línea que se especializan en los temas de arranque, como
WrapBootstrap.com (que tiene una variedad de temas comerciales) y Bootswatch.com (que ofrece temas
gratuitos). Algunas de las plantillas de pago disponibles proporcionan un amplio abanico de funcionalidades en la
parte superior del tema de Bootstrap básico, como la compatibilidad enriquecida con menús administrativos y
paneles con medidores y gráficos enriquecidos. Un ejemplo de una plantilla de pago popular es Inspinia, que se
muestra en la captura de pantalla siguiente:

Si desea cambiar el tema de Bootstrap, 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 descritos, Bootstrap incluye compatibilidad para una 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 con Bootstrap habilitado. 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 texto adicional que
proporciona el usuario con una experiencia más intuitiva:

Rutas de navegación
Las rutas de navegación 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 exploració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
vídeo y presentaciones de diapositivas con capacidad de respuesta incrustado mediante <iframe> , <embed> ,
<video> , o <object> elementos, que Bootstrap se estilo automáticamente. Especifique una relación de aspecto
determinada mediante el uso de clases específicas, como "Insertar-con capacidad de respuesta-16by9".

Compatibilidad con JavaScript


Biblioteca de JavaScript de bootstrap incluye compatibilidad con la API para los componentes incluidos, lo que
permite controlar su comportamiento mediante programación dentro de la aplicación. Además, bootstrap.js
incluye más de una docena complementos personalizados de jQuery, que proporciona características adicionales,
como las transiciones, cuadros de diálogo modales, desplácese hacia la detección (actualizando estilos según
donde se ha desplazado el usuario en el documento), comportamiento de contraer, carruseles 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 integrados en Bootstrap: para más información, visite
http://getbootstrap.com/javascript/ .

Resumen
Bootstrap ofrece un marco web que puede usarse para diseñar y una amplia variedad de sitios Web y aplicaciones
de estilo rápida y productiva. Tipografía básica y los estilos proporcionan una apariencia y funcionamiento
agradable que fácilmente se pueden manipular mediante la compatibilidad de tema personalizado, que puede ser
artesanal o adquirir comercialmente. Admite una serie de componentes web que, en el pasado, habría requerido
costosos controles de terceros para lograr, al tiempo que admite los estándares web modernos y abiertos.
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.
Agrupar y minificar recursos estáticos en ASP.NET
Core
25/09/2018 • 18 minutes to read • Edit Online

Por Scott Addie


En este artículo explica las ventajas de la aplicación de unión y minificación, incluido cómo se pueden usar estas
características con aplicaciones web ASP.NET Core.

¿Qué es la unión y minificación


Unión y minificación son dos optimizaciones de rendimiento distintas que se pueden aplicar en una aplicación
web. Se usan juntos, unión y minificación de mejoran el rendimiento mediante la reducción del número de
solicitudes de servidor y reducir el tamaño de los activos estáticos solicitados.
Unión y minificación principalmente mejoran el tiempo de carga de solicitud de primera página. Una vez que se
ha solicitado una página web, el explorador almacena en caché los recursos estáticos (imágenes, CSS y
JavaScript). Por lo tanto, unión y minificación no mejoran el rendimiento cuando se solicita la misma página o
páginas, en el mismo sitio que solicita los mismos recursos. Si el expira encabezado no está configurado
correctamente en los activos y si no se usa la unión y minificación, la heurística de actualización del explorador
marca los recursos obsoletos después de unos días. Además, el explorador requiere una solicitud de validación
para cada recurso. En este caso, unión y minificación de proporcionan una mejora del rendimiento incluso
después de la primera solicitud de página.
La Unión
La Unión combina varios archivos en un único archivo. La unión, reduce el número de solicitudes del servidor que
son necesarios para representar un activo de web, como 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 al servidor o desde el servicio que proporciona la aplicación. Este se produce en el
mejor 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 tamaño considerable en los activos solicitados (como CSS, imágenes y archivos de JavaScript). 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 la unión y minificación


En la tabla siguiente se describe las diferencias entre cargar activos y uso de unión y minificación individualmente:

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, aunque en este ejemplo se ejecutó localmente. Mejoras de rendimiento mayores se llevan a cabo
cuando el uso de unión y minificación con activos transfiere a través de una red.

Elegir una estrategia de unión y minificación


Las plantillas de proyecto MVC y páginas de Razor proporcionan una solución para la unión y minificación que
consta de un archivo de configuración de JSON. Herramientas de terceros, como el Gulp y Grunt ejecutores de
tareas, realizar las mismas tareas con un poco más compleja. Una herramienta de terceros es una opción ideal
cuando el flujo de trabajo de desarrollo requiere un procesamiento más allá de unión y minificación—como la
optimización de la detección de errores y la imagen. Mediante el uso de unión y minificación de tiempo de diseño,
se crean los archivos minimizados antes de la implementación de la aplicación. Agrupar y minificar antes de la
implementación ofrece la ventaja de carga reducida del servidor. Sin embargo, es importante reconocer ese
tiempo de diseño la unión y minificación aumenta la complejidad de la compilación y solo funciona con archivos
estáticos.

Configurar la unión y minificación


Las plantillas de proyecto MVC y 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 lote único
para el código 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 se admiten patrones.
minify : Las opciones de reducción para el tipo de salida. opcional, predeterminado:
minify: { enabled: true }
Las 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 en el archivo de proyecto.
opcional, predeterminado: false
sourceMap : Marca que indica si se debe generar un mapa de origen 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.

Compilación en tiempo de ejecución de la unión y minificación


El BuildBundlerMinifier paquete NuGet permite la ejecución de la unión y minificación en tiempo de compilación.
Inserta el paquete destinos de MSBuild que se ejecutan en la compilación y tiempo de limpieza. El
bundleconfig.json archivo analizado por el proceso de compilación para generar los archivos de salida según la
configuración definida.

NOTE
BuildBundlerMinifier pertenece a un proyecto controlado por la Comunidad en GitHub para el que Microsoft no
proporciona soporte. Se deben notificar 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 unión y minificación


Es posible ejecutar las tareas de unión y minificación en forma ad-hoc, sin compilar el proyecto. Agregar el
BundlerMinifier.Core paquete NuGet al 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 soporte. Se deben notificar problemas aquí.

Este paquete amplía la CLI de .NET Core para incluir el 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 NuGet agrega las dependencias al 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 al siguiente:
.about, [role=main], [role=complementary] {
margin-top: 60px;
}

footer {
margin-top: 10px;
}

Para minimizar custom.css y empaquetarla con site.css en un site.min.css , agregue la ruta de acceso relativa
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:

"inputFiles": ["wwwroot/**/*(*.css|!(*.min.css))"]

Este patrón global coincide con todos los archivos CSS y excluye el patrón de archivo minimizado.

Compile la aplicación. Abra site.min.css y observe el contenido de custom.css se anexa al final del archivo.

Unión y minificación basado en el entorno


Como práctica recomendada, se deben usar los archivos y minificados de la aplicación en un entorno de
producción. Durante el desarrollo, asegúrese de los archivos originales para una depuración más sencilla de la
aplicación.
Especificar qué archivos incluir en las páginas mediante el aplicación auxiliar de etiquetas de entorno en las vistas.
La aplicación auxiliar de etiquetas de entorno solo procesa su contenido cuando se ejecuta en específico entornos.
La siguiente environment etiqueta representa los archivos CSS sin procesar 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>

La siguiente environment etiqueta representa los archivos CSS y minificados cuando se ejecuta en un entorno
distinto Development . Por ejemplo, que se ejecutan en Production o Staging desencadena el procesamiento de
estas hojas de estilo:
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 de Gulp


Hay casos en que el flujo de trabajo de unión y minificación de una aplicación requiere un procesamiento
adicional. Algunos ejemplos son la optimización de la imagen, limpieza de caché y el procesamiento de activos de
red CDN. Para satisfacer estos requisitos, que puede convertir el flujo de trabajo de unión y minificación para usar
Gulp.
Use la extensión Bundler & Minifier
Visual Studio Bundler & Minifier extensión controla la conversión a Gulp.

NOTE
La extensión Bundler & Minifier pertenece a un proyecto controlado por la Comunidad en GitHub para el que Microsoft no
proporciona soporte. Se deben notificar problemas aquí.

Haga clic en el bundleconfig.json de archivos en el Explorador de soluciones y seleccione Bundler & Minifier >
convertir a Gulp... :

El gulpfile.js y package.json archivos se agregan al proyecto. La compatibilidad con npm paquetes que aparecen
en la package.json del archivo devDependencies sección están instalados.
Ejecute el siguiente comando en la ventana PMC para instalar la CLI de Gulp como una dependencia global:
npm i -g gulp-cli

El gulpfile.js lecturas de archivos el bundleconfig.json archivo para las entradas, salidas y la 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 Bundler & Minifier no está disponible, convertir manualmente.
Agregar un package.json archivo, por lo 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 comando siguiente en el mismo nivel que package.json:

npm i

Instale la CLI de Gulp como una dependencia global:

npm i -g gulp-cli

Copia el 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$/
};
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 las tareas de Gulp


Para desencadenar la tarea de Gulp minificación antes de que el proyecto se compila en Visual Studio, agregue el
siguiente 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 ==========

Como alternativa, Visual Studio Task Runner Explorer puede utilizarse para enlazar las tareas de Gulp a eventos
específicos de Visual Studio. Consulte ejecutando tareas predeterminadas para obtener instrucciones sobre cómo
hacerlo.

Recursos adicionales
Uso de Gulp
Uso de Grunt
Uso de varios entornos
Asistentes de etiquetas
Vínculo de explorador en ASP.NET Core
23/08/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, que es útil para probar varios exploradores.

Configuración de 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 metapaquete, instale el Microsoft.VisualStudio.Web.BrowserLink del paquete de
Funcionalidad de BrowserLink. Usan las plantillas de proyecto de ASP.NET Core 2.1 la Microsoft.AspNetCore.App
metapaquete de forma predeterminada.
ASP.NET Core 2.0 aplicación Web, vacía, y API Web uso de plantillas de proyecto la metapaquete
Microsoft.AspNetCore.All , que contiene una referencia de paquete para Microsoft.VisualStudio.Web.BrowserLink.
Por lo tanto, uso el Microsoft.AspNetCore.All metapaquete requiere ninguna acción para que el vínculo de
explorador esté disponible para su uso.
ASP.NET Core 1.x aplicación Web plantilla de proyecto tiene una referencia de paquete para el
Microsoft.VisualStudio.Web.BrowserLink paquete. El vacía o API Web proyectos de plantilla requieren que
agregue una referencia de paquete para Microsoft.VisualStudio.Web.BrowserLink .
Puesto que esta es una característica de Visual Studio, la manera más fácil para agregar el paquete a un vacía o
API Web es abrir el proyecto de plantilla el Package Manager Console (Vista > Other Windows > Package
Manager Console) y ejecute el siguiente comando:

install-package Microsoft.VisualStudio.Web.BrowserLink

Como alternativa, puede usar Administrador de paquetes de NuGet. Haga clic en el nombre del proyecto en el
Explorador de soluciones y elija administrar paquetes de NuGet:
Buscar e instalar el paquete:

Configuración
En el método Startup.Configure :

app.UseBrowserLink();

Normalmente es el código dentro de un if bloque que permite solo 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 tiene abierto un proyecto de ASP.NET Core, Visual Studio muestra el control de barra de herramientas de
vínculo de explorador junto a la depurar destino toolbar (control):

Desde el control de barra de herramientas vínculo de explorador, hacer lo siguiente:


Actualizar la aplicación web a la vez en varios exploradores.
Abra el panel vínculo de explorador.
Habilitar o deshabilitar Browser Link. 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 Web extensión Pack 2015 y Web extensión Pack 2017, ofrecen
funcionalidad ampliada de vínculo de explorador, pero algunas de las características adicionales no funcionan con ASP.
Proyectos de .NET Core.

Actualizar la aplicación web en varios exploradores a la vez


Para elegir un explorador web única para iniciar al iniciar el proyecto, use el menú desplegable en el depurar
destino toolbar (control):

Para abrir a la vez varios exploradores, 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:
Este 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:
Cambiar la vista de índice y se actualizan todos los exploradores conectados al hacer clic en el botón de
actualización de Browser Link:

Vínculo de explorador también funciona con los 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
Abra el panel de vínculos de explorador en el menú para administrar la conexión con los exploradores Abrir
desplegable Browser Link:
Si no hay ningún explorador está conectado, puede iniciar una sesión de no depuración seleccionando el ver en el
explorador vínculo:

En caso contrario, se muestran los exploradores conectados con la ruta de acceso a la página que muestra cada
explorador:
Si lo desea, puede hacer clic en un nombre de lista del explorador para actualizar ese explorador único.
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
conectarse a ellos.
Habilitar o deshabilitar la sincronización automática de CSS
Cuando se habilita la sincronización automática de CSS, los 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 de ASP.NET Core. 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 desplácese 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 se 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 todas, funciona en todos los exploradores compatibles
con SignalR sin necesidad de un complemento de explorador.
Uso de JavaScriptServices para crear aplicaciones de
página única en ASP.NET Core
21/09/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.

¿Qué es JavaScriptServices
JavaScriptServices es una colección de tecnologías de cliente para ASP.NET Core. Su objetivo es a la posición de
ASP.NET Core como plataforma de servidor preferido de los desarrolladores para la creación de spa.
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 es:
Ejecutar JavaScript en el servidor
Usa un marco o biblioteca de SPA
Compile los recursos del lado cliente con Webpack
Gran parte el foco en este artículo se coloca sobre el uso del 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 proporciona infraestructura útil, como:
Procesamiento previo del lado servidor
Middleware de desarrollo de Webpack
Sustitución del módulo de acceso frecuente
Aplicaciones auxiliares de enrutamientos
De forma colectiva, 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, instale lo siguiente:
Node.js (versión 6 o posterior) con npm
Para comprobar estos componentes se instalan y se pueden encontrar, 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á instalado y
disponible en los entornos de servidor.
.NET Core SDK 2.0 o posterior
Si se encuentra en Windows con Visual Studio 2017, el SDK se instala seleccionando el desarrollo
multiplataforma de .NET Core carga de trabajo.
Microsoft.AspNetCore.SpaServices paquete NuGet

Procesamiento previo del lado 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 lado servidor mediante la invocación de las funciones de JavaScript en el servidor.
Requisitos previos
Instale el software siguiente:
ASPNET-procesamiento previo paquete npm:

npm i -S aspnet-prerendering

Configuración
Las aplicaciones auxiliares de etiquetas se hacen reconocibles a través del 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 las complejidades de la comunicación directa con la API de bajo
nivel mediante el aprovechamiento de una sintaxis similar a HTML dentro de la vista de Razor:

<app asp-prerender-module="ClientApp/dist/main-server">Loading...</app>

El asp-prerender-module aplicación auxiliar de etiquetas


El asp-prerender-module ejecuta la aplicación auxiliar de etiquetas, utilizados 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 transpilación de TypeScript y JavaScript en el Webpack proceso de compilación. Webpack
define un alias de punto de entrada de main-server ; y comienza el cruce seguro de que el gráfico de dependencias
para este alias 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 resolución, que se ajusta en JavaScript fuertemente tipado Promise objeto. El Promise es de
importancia del objeto que proporciona asincrónicamente el marcado HTML para la página para la inserción en el
elemento de DOM 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 aplicación auxiliar de etiquetas


Cuando se combina con la asp-prerender-module aplicación auxiliar de etiquetas, la asp-prerender-data aplicación
auxiliar de etiquetas se puede usar para pasar información contextual de la vista de Razor a JavaScript del lado
servidor. Por ejemplo, el marcado siguiente 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 las aplicaciones auxiliares de etiquetas se representan con
PascalCase notación. Compare esto con JavaScript, donde se representan los mismos nombres de propiedad con
camelCase. La configuración predeterminada de 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 a la 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 está adjunto al explorador global window objeto. Esta
elevación de variables en ámbito global elimina la duplicación de esfuerzos, sobre todo lo que respecta a la carga
de los mismos datos una vez en el servidor y de nuevo en el cliente.

Middleware de desarrollo de Webpack


Middleware de desarrollo de Webpack incorpora un flujo de trabajo de desarrollo optimizado por el cual Webpack
compila los 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 enfoque alternativo es invocar manualmente la Webpack a través
de script de compilación del proyecto npm cuando cambia una dependencia de terceros o código personalizado.
Script de compilación de un npm 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 paquete npm:

npm i -D aspnet-webpack

Configuración
Middleware de desarrollo de 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 debe llamarse antes registrar archivos estáticos de 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 inspeccionar el dist
carpeta para los 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 frecuente


Piense de Webpack reemplazo en caliente módulo característica (HMR ) como una evolución de Webpack Dev
Middleware. HMR presenta las mismas ventajas, pero simplifica aún más el flujo de trabajo de desarrollo
actualizando automáticamente el contenido de la página después de compilar los cambios. No lo confunda 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 SPA. Hay un vínculo directo entre el servicio de Webpack Dev Middleware y el explorador, lo que significa
que los cambios se insertan en el explorador.
Requisitos previos
Instale el software siguiente:
middleware "hot" webpack paquete npm:

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

Igual que ocurría con Webpack Dev Middleware, UseWebpackDevMiddleware método de extensión debe llamarse
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 un plugins matriz, incluso si se deja vacío:

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 la confirmación de la activación de HMR:

Aplicaciones auxiliares de enrutamientos


En la mayoría de las spa basada en ASP.NET Core, le conviene además del enrutamiento del servidor de
enrutamiento del lado cliente. Los sistemas de enrutamiento SPA y MVC pueden trabajar de forma independiente
sin interferencias. Hay, sin embargo, un borde case plantean desafíos: identificación de respuestas HTTP 404.
Considere el escenario en el que una ruta sin extensión de /some/page se utiliza. Suponga que la solicitud no
hacer coincidir el patrón una ruta de servidor, pero su patrón coincide con una ruta del lado 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 del lado servidor o
un archivo estático, no es probable que la aplicación del lado cliente controlaría, por lo general desea devolver un
código de estado HTTP 404.
Requisitos previos
Instale el software siguiente:
El paquete de npm enrutamiento del lado cliente. Uso de 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 se está configurado. Por lo tanto, el default ruta en el
ejemplo de código anterior se usa primero para la coincidencia de patrones.

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 .NET Core, ejecute el comando siguiente:

dotnet new --install Microsoft.AspNetCore.SpaTemplates::*

Se muestra una lista de plantillas SPA disponibles:

PLANTILLAS NOMBRE CORTO LENGUAJE ETIQUETAS

MVC de ASP.NET Core con angular [C#] MVC/Web/SPA


Angular

MVC de ASP.NET Core con react [C#] MVC/Web/SPA


React.js

MVC ASP.NET Core con reactredux [C#] MVC/Web/SPA


React.js y Redux

Para crear un nuevo proyecto con una de las plantillas SPA, incluya el nombre corto de la plantilla en el dotnet
nuevo comando. El siguiente comando crea una aplicación Angular con ASP.NET Core MVC configurada 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:
Incluye mapas de código fuente 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 del lado cliente a través de la unión y minificación.
ASP.NET Core usa una variable de entorno denominada ASPNETCORE_ENVIRONMENT para almacenar el modo de
configuración. Consulte establecer el entorno para obtener más información.
Ejecutar con la CLI de .NET Core
Restaurar los paquetes de npm y NuGet necesario ejecutando el siguiente comando en la raíz del proyecto:

dotnet restore && npm i

Compilar y ejecutar la aplicación:

dotnet run

Se inicia la aplicación en el host local según el modo de configuración en tiempo de ejecución. Vaya a
http://localhost:5000 en el explorador muestra la página de inicio.

Ejecución con Visual Studio 2017


Abra el .csproj archivo generado por el dotnet nuevo comando. Los paquetes de NuGet y npm necesarios se
restauran automáticamente al proyecto abierto. Este proceso de restauración puede tardar varios minutos, y la
aplicación está lista para ejecutarse cuando se complete. Haga clic en el botón verde de ejecución o presione
Ctrl + F5 , y el explorador se abre en la página de aterrizaje de la aplicación. La aplicación se ejecuta en localhost
según la 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.
Con la aplicación Angular como ejemplo, dos casos de prueba Jasmine ya se proporcionan para el
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

El script 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 que se ejecuta 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 recursos del lado cliente generados y los artefactos de ASP.NET Core publicados en un
paquete listo para implementar puede resultar tedioso. Afortunadamente, SpaServices organiza 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 de npm
2. Crear una compilación de producción de los recursos de otros fabricantes, del lado cliente
3. Crear una compilación de producción de los activos de cliente personalizadas
4. Copiar los activos de Webpack generados en la carpeta de publicación
El destino de MSBuild se invoca cuando se ejecuta:

dotnet publish -c Release

Recursos adicionales
Docs 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 o posterior
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
18/09/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 es necesario instalar la plantilla de proyecto de 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
18/09/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 es necesario instalar la plantilla de proyecto de React.
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.
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
12/09/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. Azure DevOps 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.

Hospedaje en una granja de servidores web


Para obtener más información sobre la configuración del hospedaje de aplicaciones de ASP.NET Core en un
entorno de granja de servidores web (por ejemplo, para implementar varias instancias de la aplicación para
escalabilidad), consulte Hospedaje de ASP.NET Core en una granja de servidores web.

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
12/09/2018 • 15 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.
Creación de la primera canalización con Azure Pipelines
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, los siguientes paquetes de NuGet proporcionan características de
registro automáticas para aplicaciones implementadas en Azure App Service:
Microsoft.AspNetCore.AzureAppServices.HostingStartup usa IHostingStartup para proporcionar a ASP.NET
Core una integración ligera (Light-Up) 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.
Si tiene .NET Core como destino y hace referencia al metapaquete Microsoft.AspNetCore.All, los paquetes ya
están incluidos. Los paquetes no están en el metapaquete Microsoft.AspNetCore.App, que es más reciente. Si
tiene .NET Framework como destino o hace referencia al metapaquete Microsoft.AspNetCore.App , haga referencia
a los paquetes de registro individuales.

Invalidación de la configuración de la aplicación mediante Azure Portal


El área Configuración de la aplicación de la hoja Configuración de la aplicación le permite establecer
variables de entorno para la aplicación. El proveedor de configuración de variables de entorno puede consumir las
variables de entorno.
Cuando una aplicación usa el host web y compila el host mediante WebHost.CreateDefaultBuilder, las variables de
entorno que configuran el host usan el prefijo ASPNETCORE_ . Para más información, consulte Host web de
ASP.NET Core y el proveedor de configuración de variables de entorno.
Cuando una aplicación usa el host genérico, no se cargan variables de entorno en la configuración de una
aplicación de manera predeterminada y es el desarrollador el que debe agregar al proveedor de configuración. El
desarrollador determina el prefijo de variable de entorno cuando se agrega el proveedor de configuración. Para
más información, consulte Host genérico de .NET y el proveedor de configuración de variables de entorno.

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
Conozca los métodos habituales 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
Usar Docker con Web Apps para contenedores
Instalación de la extensión de sitio de versión preliminar
Si tiene algún problema al usar la extensión de sitio de versión preliminar, abra una incidencia en GitHub.
1. En Azure Portal, vaya a la hoja de App Service.
2. Seleccione la aplicación web.
3. Escriba "ex" en el cuadro de búsqueda o desplácese hacia abajo en la lista de secciones de administración hasta
llegar a HERRAMIENTAS DE DESARROLLO.
4. Seleccione HERRAMIENTAS DE DESARROLLO > Extensiones.
5. Seleccione Agregar.
6. Seleccione la extensión Runtime de ASP.NET Core <x.y> (x86) en la lista, donde <x.y> es la versión
preliminar de ASP.NET Core (por ejemplo, Runtime de ASP.NET Core 2.2 (x86)). Runtime de x86 es
adecuado para implementaciones dependientes del marco, que se basan en el hospedaje fuera de proceso
proporcionado por el módulo ASP.NET Core.
7. Seleccione Aceptar para aceptar los términos legales.
8. Seleccione Aceptar para instalar la extensión.
Cuando se complete la operación, se instalará la versión preliminar de .NET Core. Compruebe la instalación:
1. Seleccione Herramientas avanzadas en HERRAMIENTAS DE DESARROLLO.
2. Seleccione Ir en la hoja Herramientas avanzadas.
3. Seleccione el elemento de menú Consola de depuración > PowerShell.
4. Ejecute el siguiente comando en el símbolo del sistema de PowerShell: Sustituya la versión de runtime de
ASP.NET Core por <x.y> en el comando:

Test-Path D:\home\SiteExtensions\AspNetCoreRuntime.<x.y>.x86\

Si el runtime de la versión preliminar instalada es para ASP.NET Core 2.2, el comando será:

Test-Path D:\home\SiteExtensions\AspNetCoreRuntime.2.2.x86\

El comando devuelve True cuando está instalado el runtime de la versión preliminar de x64.

NOTE
La arquitectura de plataforma (x86/x64) de una aplicación de App Services se configura en la hoja Configuración de la
aplicación, en Configuración general, para las aplicaciones hospedadas en un cálculo de serie A o en un mejor nivel de
hospedaje. Si la aplicación se ejecuta en modo de en proceso y la arquitectura de plataforma está configurada para 64 bits
(x64), el módulo ASP.NET Core usa el runtime de la versión preliminar de 64 bits, si está presente. Instale la extensión
Runtime de ASP.NET Core <x.y> (x64) (por ejemplo, Runtime de ASP.NET Core 2.2 (x64)).
Después de instalar el runtime de la versión preliminar de x64, ejecute el siguiente comando en la ventana de comandos de
Kudu PowerShell para comprobar la instalación. Sustituya la versión de runtime de ASP.NET Core por <x.y> en el comando:

Test-Path D:\home\SiteExtensions\AspNetCoreRuntime.<x.y>.x64\

Si el runtime de la versión preliminar instalada es para ASP.NET Core 2.2, el comando será:

Test-Path D:\home\SiteExtensions\AspNetCoreRuntime.2.2.x64\

El comando devuelve True cuando está instalado el runtime de la versión preliminar de x64.

NOTE
Con Extensiones de ASP.NET Core se obtienen funciones adicionales para ASP.NET Core en Azure App Services, como
habilitar el registro de Azure. La extensión se instala automáticamente cuando se implementa desde Visual Studio. Si no se
instala la extensión, instálela para la aplicación.

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

Usar Docker con Web Apps para contenedores


Docker Hub contiene las imágenes de Docker de versión preliminar 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
Hospedaje de ASP.NET Core en una granja de servidores web
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
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
30/07/2018 • 7 minutes to read • Edit Online

De Rick Anderson, Cesar Blum Silveira y Rachel Appel

IMPORTANT
Versiones preliminares de ASP.NET Core con Azure App Service
Las versiones preliminares de ASP.NET Core no se implementan de forma predeterminada en Azure App Service. Para
hospedar una aplicación que usa una versión preliminar de ASP.NET Core, vea 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
Implementación continua en Azure con Visual Studio
y Git con ASP.NET Core
31/08/2018 • 13 minutes to read • Edit Online

Por Erik Reitan

IMPORTANT
Versiones preliminares de ASP.NET Core con Azure App Service
Las versiones preliminares de ASP.NET Core no se implementan de forma predeterminada en Azure App Service. Para
hospedar una aplicación que usa una versión preliminar de ASP.NET Core, vea 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 o posterior
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
27/08/2018 • 19 minutes to read • Edit Online

Por Luke Latham

IMPORTANT
Versiones preliminares de ASP.NET Core con Azure App Service
Las versiones preliminares de ASP.NET Core no se implementan de forma predeterminada en Azure App Service. Para
hospedar una aplicación que usa una versión preliminar de ASP.NET Core, vea 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
24/07/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. CreateDefaultBuilder
llama al método UseIISIntegration, que 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 del archivo
web.config se controla por medio de un destino de MSBuild ( _TransformWebConfig ) cuando el proyecto se
publica. Este destino está incluido entre los destinos del SDK web ( Microsoft.NET.Sdk.Web ). El SDK se establece
al inicio 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 descargas de .NET.
b. En .NET Core, seleccione el botón Download .NET Core Runtime (Descargar .NET Core Runtime),
junto a la etiqueta Run Apps (Ejecutar aplicaciones). El archivo ejecutable del instalador contiene la
palabra "hosting" en el nombre de archivo, por ejemplo, dotnet-hosting -2.1.2 -win.exe.
c. Ejecute el instalador en el servidor.
¡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, que es una variable de entorno,
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 (ASP.NET Core 2.2) 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, nuestra recomendación es aislar las aplicaciones entre sí
ejecutándolas 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
31/08/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.
Depuración con Visual Studio Code
Obtenga información sobre la compatibilidad de depuración integrada en Visual Studio Code.

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. Asegúrese de que la identidad del grupo de aplicaciones tiene permisos de escritura en la carpeta logs.
5. Guarde el archivo web.config actualizado.
6. Realice una solicitud a la aplicación.
7. Vaya a la carpeta logs. Busque y abra el registro más reciente de stdout.
8. 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
24/09/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 DESCRIPCIÓN 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 DESCRIPCIÓN 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 DESCRIPCIÓN 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 DESCRIPCIÓN 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 DESCRIPCIÓN 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 DESCRIPCIÓN 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 resultados de consola 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
29/06/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 para Windows.
Carga de trabajo de ASP.NET y desarrollo web
Carga de trabajo Desarrollo multiplataforma de .NET Core
Certificado de seguridad X.509

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
03/09/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/08/2018 • 12 minutes to read • Edit Online

Por Luke Latham y Tom Dykstra


Una aplicación de ASP.NET Core se puede hospedar en Windows sin usar IIS como 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)

Primeros pasos
Estos son los cambios mínimos necesarios para configurar un proyecto de ASP.NET Core existente para que se
ejecute en un servicio:
1. El archivo del proyecto:
a. Confirme la presencia del identificador en tiempo de ejecución o agregarlo al <PropertyGroup>
que contiene el marco de destino:

<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<RuntimeIdentifier>win7-x64</RuntimeIdentifier>
</PropertyGroup>

<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
<RuntimeIdentifier>win7-x64</RuntimeIdentifier>
</PropertyGroup>

<PropertyGroup>
<TargetFramework>netcoreapp1.1</TargetFramework>
<RuntimeIdentifier>win7-x64</RuntimeIdentifier>
</PropertyGroup>

b. Agregue una referencia de paquete de Microsoft.AspNetCore.Hosting.WindowsServices.


2. Realice los siguientes cambios en Program.Main .
Llame a host.RunAsService en lugar de a host.Run .
Llame a UseContentRoot y use una ruta de acceso a la ubicación de publicación de la aplicación en
lugar de Directory.GetCurrentDirectory() .
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().RunAsService();
}

public static IWebHostBuilder CreateWebHostBuilder(string[] args)


{
var pathToExe = Process.GetCurrentProcess().MainModule.FileName;
var pathToContentRoot = Path.GetDirectoryName(pathToExe);

return WebHost.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((context, config) =>
{
// Configure the app here.
})
.UseContentRoot(pathToContentRoot)
.UseStartup<Startup>();
}

public static void Main(string[] args)


{
var pathToExe = Process.GetCurrentProcess().MainModule.FileName;
var pathToContentRoot = Path.GetDirectoryName(pathToExe);

var host = new WebHostBuilder()


.UseKestrel()
.UseContentRoot(pathToContentRoot)
.UseIISIntegration()
.UseStartup<Startup>()
.Build();

host.RunAsService();
}

3. Publique la aplicación. Use dotnet publish o un perfil de publicación de Visual Studio. Al utilizar Visual
Studio, seleccione FolderProfile.
Para publicar la aplicación de ejemplo desde la línea de comandos, ejecute el siguiente comando en una
ventana de la consola desde la carpeta de proyecto:

dotnet publish --configuration Release

4. Use la herramienta de línea de comandos sc.exe para crear el servicio. El valor binPath es la ruta de acceso
al archivo ejecutable de la aplicación, que incluye el nombre del archivo ejecutable. El espacio entre el
signo igual y las comillas al inicio de la cadena de ruta de acceso es obligatorio.

sc create <SERVICE_NAME> binPath= "<PATH_TO_SERVICE_EXECUTABLE>"

En el caso de un servicio publicado en la carpeta del proyecto, use la ruta de acceso a la carpeta publish para
crear el servicio. En el ejemplo siguiente:
El proyecto se encuentra en la carpeta c:\my_services\AspNetCoreService .
El proyecto se publica en la configuración Release .
El moniker de la plataforma de destino (TFM ) es netcoreapp2.1 .
El identificador del entorno de ejecución (RID ) es win7-x64 .
El ejecutable de la aplicación se denomina AspNetCoreService.exe.
El servicio se denomina MyService.
Ejemplo:

sc create MyService binPath= "c:\my_services\AspNetCoreService\bin\Release\netcoreapp2.1\win7-


x64\publish\AspNetCoreService.exe"

IMPORTANT
No olvide incluir el espacio entre el argumento binPath= y su valor.

Para publicar e iniciar el servicio desde otra carpeta:


a. Use la opción --output <DIRECTORIO_DE_SALIDA> en el comando dotnet publish . Si utiliza Visual
Studio, configure el valor Ubicación de destino de la página de la propiedad de publicación
FolderProfile antes de hacer clic en el botón Publicar.
b. Cree el servicio con el comando sc.exe utilizando la ruta de acceso de la carpeta de salida. Incluya el
nombre del archivo ejecutable del servicio en la ruta de acceso proporcionada a binPath .
5. Inicie el servicio con el comando sc start <SERVICE_NAME> .
Use el siguiente comando para iniciar el servicio de la aplicación de ejemplo:

sc start MyService

Este comando tarda unos segundos en iniciar el servicio.


6. El comando sc query <SERVICE_NAME> se puede usar para comprobar el estado del servicio con objeto de
conocer su estado:
START_PENDING
RUNNING
STOP_PENDING
STOPPED
Use el siguiente comando para comprobar el estado del servicio de la aplicación de ejemplo:

sc query MyService

7. Si el servicio está en estado RUNNING y dicho servicio es una aplicación web, vaya a la aplicación en su ruta
de acceso correspondiente (de forma predeterminada, http://localhost:5000 , que redirige a
https://localhost:5001 cuando se usa el Middleware de redirección de HTTPS ).

Si es el servicio de la aplicación de ejemplo, vaya a la aplicación en http://localhost:5000 .


8. Detenga el servicio con el comando sc stop <SERVICE_NAME> .
Con el siguiente comando se detiene el servicio de la aplicación de ejemplo:

sc stop MyService

9. Tras un breve intervalo para detener un servicio, desinstale el servicio con el comando
sc delete <SERVICE_NAME> .

Compruebe el estado del servicio de la aplicación de ejemplo:


sc query MyService

Si el servicio de la aplicación de ejemplo está en estado STOPPED , use el siguiente comando para desinstalar
el servicio de la aplicación de ejemplo:

sc delete MyService

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:

public static void Main(string[] args)


{
var isService = !(Debugger.IsAttached || args.Contains("--console"));
var builder = CreateWebHostBuilder(args.Where(arg => arg != "--console").ToArray());

if (isService)
{
var pathToExe = Process.GetCurrentProcess().MainModule.FileName;
var pathToContentRoot = Path.GetDirectoryName(pathToExe);
builder.UseContentRoot(pathToContentRoot);
}

var host = builder.Build();

if (isService)
{
host.RunAsService();
}
else
{
host.Run();
}
}

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>


WebHost.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((context, config) =>
{
// Configure the app here.
})
.UseStartup<Startup>();

Dado que la configuración de ASP.NET Core requiere pares nombre-valor en los argumentos de línea de
comandos, el conmutador --console se quita antes de que los argumentos se pasen a CreateDefaultBuilder.

NOTE
isService no se pasa de Main a CreateWebHostBuilder porque la firma de CreateWebHostBuilder debe ser
CreateWebHostBuilder(string[]) para que las pruebas de integración funcionen correctamente.
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 host = new WebHostBuilder()


.UseKestrel()
.UseContentRoot(pathToContentRoot)
.UseIISIntegration()
.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, haga los siguientes cambios adicionales:
1. Cree una clase que se derive de la clase 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. Cree 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:

public static void Main(string[] args)


{
var isService = !(Debugger.IsAttached || args.Contains("--console"));
var builder = CreateWebHostBuilder(args.Where(arg => arg != "--console").ToArray());

if (isService)
{
var pathToExe = Process.GetCurrentProcess().MainModule.FileName;
var pathToContentRoot = Path.GetDirectoryName(pathToExe);
builder.UseContentRoot(pathToContentRoot);
}

var host = builder.Build();

if (isService)
{
host.RunAsCustomService();
}
else
{
host.Run();
}
}

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>


WebHost.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((context, config) =>
{
// Configure the app here.
})
.UseStartup<Startup>();

NOTE
isService no se pasa de Main a CreateWebHostBuilder porque la firma de CreateWebHostBuilder debe ser
CreateWebHostBuilder(string[]) para que las pruebas de integración funcionen correctamente.
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 host = new WebHostBuilder()


.UseKestrel()
.UseContentRoot(pathToContentRoot)
.UseIISIntegration()
.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 IWebHost.Services:
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 obtener más información, vea
Configuración de ASP.NET Core para trabajar con servidores proxy y equilibradores de carga.

Configuración de HTTPS
Especifique una configuración de punto de conexión HTTPS de servidor Kestrel.

Directorio actual y raíz del contenido


El directorio de trabajo actual devuelto por una llamada a Directory.GetCurrentDirectory() para un servicio de
Windows es la carpeta C:\WINDOWS\system32. La carpeta system32 no es una ubicación adecuada para
almacenar los archivos de un servicio (por ejemplo los archivos de configuración). Use uno de los enfoques
siguientes para mantener y acceder a los recursos y los archivos de configuración de un servicio con
FileConfigurationExtensions.SetBasePath cuando se usa una interfaz IConfigurationBuilder:
Use la ruta de acceso raíz del contenido. IHostingEnvironment.ContentRootPath es la misma ruta de acceso
proporcionada al argumento binPath cuando se crea el servicio. En lugar de usar
Directory.GetCurrentDirectory() para crear rutas de acceso a los archivos de configuración, use la ruta de
acceso raíz del contenido y mantenga los archivos en la raíz de contenido de la aplicación.
Almacene los archivos en una ubicación adecuada en el disco. Especifique una ruta de acceso absoluta con
SetBasePath a la carpeta que contiene los archivos.

Recursos adicionales
Kestrel: configuración de punto de conexión (configuración de HTTPS y compatibilidad de SNI incluidas)
Host web de ASP.NET Core
Hospedar ASP.NET Core en Linux con Nginx
12/09/2018 • 23 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. Probablemente estas instrucciones sean válidas también con versiones más recientes de Ubuntu,
pero no se han probado con versiones más recientes.
Para información sobre otras distribuciones de Linux compatibles con ASP.NET Core, consulte Requisitos
previos para .NET Core en Linux.

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. Para obtener instrucciones de Ubuntu 14.04, vea la versión anterior de este tema.

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. Tener .NET Core Runtime instalado en el servidor.
a. Vaya a la página de descargas de .NET Core.
b. Seleccione el tiempo de ejecución más reciente que no sea versión preliminar en la lista Runtime.
c. Selecciónelo y siga las instrucciones de Ubuntu correspondientes a la versión de Ubuntu del servidor.
3. Disponer de una aplicación de ASP.NET Core existente.

Publicar y copiar en la aplicación


Configure la aplicación para una implementación dependiente de Framework.
Ejecute dotnet publish desde el entorno de desarrollo para empaquetar una aplicación en un directorio (por
ejemplo, bin/Release/<target_framework_moniker>/publish) que se pueda ejecutar en el servidor:

dotnet publish --configuration Release

La aplicación también se puede publicar como una implementación independiente si prefiere no mantener .NET
Core Runtime en el servidor.
Copie la aplicación de ASP.NET Core en el servidor usando una herramienta que se integre en el flujo de
trabajo de la organización (como SCP o SFTP ). Lo habitual es encontrar las aplicaciones web en el directorio var
(por ejemplo, aspnetcore/var/hellomvc).
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.

Pruebe la aplicación:
1. Desde la línea de comandos, ejecute la aplicación: dotnet <app_assembly>.dll .
2. En un explorador, vaya a http://<serveraddress>:<port> para comprobar que la aplicación funciona en Linux
de forma local.

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.

NOTE
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).

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.
Cualquier componente que dependa del esquema (como la autenticación, la generación de vínculos, los
redireccionamientos o la geolocalización) debe colocarse después de invocar al Middleware de encabezados
reenviados. Como norma general, el Middleware de encabezados reenviados se debe ejecutar antes de
cualquier otro middleware, salvo el middleware de diagnóstico y control de errores. Hacerlo en ese orden
garantiza que el middleware que se basa en la información de encabezados reenviados pueda usar los valores
de encabezado para procesarlos.
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
Use apt-get para instalar Nginx. El instalador crea un script de inicio systemd que ejecuta Nginx como
demonio al iniciarse el sistema.

sudo -s
nginx=stable # use nginx=development for latest development version
add-apt-repository ppa:nginx/$nginx
apt-get update
apt-get install nginx

nginx.org no distribuye el archivo de paquete personal de Ubuntu (PPA), sino que lo mantienen usuarios
voluntarios. Para más información, vea Nginx: Binary Releases: Official Debian/Ubuntu packages (Nginx:
Versiones binarias: paquetes oficiales de Debian/Ubuntu).

NOTE
Si se necesitan módulos de Nginx opcionales, puede que haya que compilar Nginx desde el origen.

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. La página de aterrizaje
está accesible en http://<server_IP_address>/index.nginx-debian.html .
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 $host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

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). Para cambiar la
IP o el puerto de Kestrel, vea Kestrel: configuración de punto de conexión.

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 .
Para ejecutar la aplicación directamente en el servidor:
1. Vaya al directorio de la aplicación.
2. Ejecute el archivo ejecutable de la aplicación: ./<app_executable> .
Si se produce un error de permisos, cambie los permisos:

chmod u+x <app_executable>

Si la aplicación se ejecuta en el servidor, pero no responde a través de Internet, compruebe el firewall del
servidor y confirme que el puerto 80 está abierto. Si está usando una máquina virtual Ubuntu de Azure,
agregue una regla de grupo de seguridad de red que posibilite el tráfico entrante del puerto 80. No es necesario
para habilitar una regla de tráfico saliente en el puerto 80, ya que dicho tráfico se concede automáticamente al
habilitar la regla de entrada.
Cuando termine de probar la aplicación, ciérrela con Ctrl+C en el símbolo del sistema.

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
# Restart service after 10 seconds if the dotnet service crashes:
RestartSec=10
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 datos
Hay varios softwares intermedios de ASP.NET Core que utilizan la pila de protección de datos de ASP.NET
Core, incluidos los de autenticación, como el de cookies, y las protecciones de falsificación de solicitud entre
sitios (CSRF ). Aunque el código de usuario no llame a las API de protección de datos, esta se debe configurar
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 de modo que sea persistente y permita cifrar el anillo de claves, consulte:
Proveedores de almacenamiento de claves en ASP.NET Core
Cifrado de claves en reposo en ASP.NET Core

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.

WARNING
Si tiene algún firewall activado y no está configurado correctamente, este impedirá el acceso a todo el sistema. Si usa SSH
para establecer la conexión y no especifica el puerto SSH correcto, quedará bloqueado y no podrá acceder al sistema. El
puerto predeterminado es el 22. Para obtener más información, consulte la introducción a UFW y el manual.

Instale ufw y configúrelo para permitir el tráfico en los puertos que convenga.

sudo apt-get install ufw

sudo ufw allow 22/tcp


sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

sudo ufw enable

Proteger Nginx
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 opciones
Configure el servidor con más módulos que sean necesarios. Sopese la posibilidad de usar un firewall de
aplicación web como ModSecurity para proteger la aplicación.
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.

Recursos adicionales
Requisitos previos para .NET Core en Linux
Nginx: Binary Releases: Official Debian/Ubuntu packages (Nginx: Versiones binarias: paquetes oficiales de
Debian/Ubuntu)
Configurar ASP.NET Core para trabajar con servidores proxy y equilibradores de carga
NGINX: Using the Forwarded header (NGINX: Uso del encabezado Forwarded)
Hospedar ASP.NET Core en Linux con Apache
02/08/2018 • 21 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. Tener .NET Core Runtime instalado en el servidor.
a. Vaya a la página de descargas de .NET Core.
b. Seleccione el tiempo de ejecución más reciente que no sea versión preliminar en la lista Runtime.
c. Seleccione y siga las instrucciones relativas a CentOS/Oracle.
3. Disponer de una aplicación de ASP.NET Core existente.

Publicar y copiar en la aplicación


Configure la aplicación para una implementación dependiente de Framework.
Ejecute dotnet publish desde el entorno de desarrollo para empaquetar una aplicación en un directorio (por
ejemplo, bin/Release/<target_framework_moniker>/publish) que se pueda ejecutar en el servidor:

dotnet publish --configuration Release

La aplicación también se puede publicar como una implementación independiente si prefiere no mantener .NET
Core Runtime en el servidor.
Copie la aplicación de ASP.NET Core en el servidor usando una herramienta que se integre en el flujo de
trabajo de la organización (como SCP o SFTP ). Lo habitual es encontrar las aplicaciones web en el directorio var
(por ejemplo, aspnetcore/var/hellomvc).

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.
Cualquier componente que dependa del esquema (como la autenticación, la generación de vínculos, los
redireccionamientos o la geolocalización) debe colocarse después de invocar al Middleware de encabezados
reenviados. Como norma general, el Middleware de encabezados reenviados se debe ejecutar antes de
cualquier otro middleware, salvo el middleware de diagnóstico y control de errores. Hacerlo en ese orden
garantiza que el middleware que se basa en la información de encabezados reenviados pueda usar los valores
de encabezado para procesarlos.

NOTE
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).

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
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 *:*>
RequestHeader set "X-Forwarded-Proto" expr=%{REQUEST_SCHEME}
</VirtualHost>

<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 . Para cambiar la IP o el puerto de Kestrel, vea Kestrel: configuración de punto de
conexión.

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 the 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 datos
Hay varios softwares intermedios de ASP.NET Core que utilizan la pila de protección de datos de ASP.NET
Core, incluidos los de autenticación, como el de cookies, y las protecciones de falsificación de solicitud entre
sitios (CSRF ). Aunque el código de usuario no llame a las API de protección de datos, esta se debe configurar
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 de modo que sea persistente y permita cifrar el anillo de claves, consulte:
Proveedores de almacenamiento de claves en ASP.NET Core
Cifrado de claves en reposo en ASP.NET Core

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 *:*>
RequestHeader set "X-Forwarded-Proto" expr=%{REQUEST_SCHEME}
</VirtualHost>

<VirtualHost *:80>
RewriteEngine On
RewriteCond %{HTTPS} !=on
RewriteRule ^/?(.*) https://%{SERVER_NAME}/$1 [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 *:*>
RequestHeader set "X-Forwarded-Proto" expr=%{REQUEST_SCHEME}
</VirtualHost>

<VirtualHost *:80>
RewriteEngine On
RewriteCond %{HTTPS} !=on
RewriteRule ^/?(.*) https://%{SERVER_NAME}/$1 [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>

Recursos adicionales
Configurar ASP.NET Core para trabajar con servidores proxy y equilibradores de carga
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
02/08/2018 • 18 minutes to read • Edit Online

Visual Studio 2017 permite compilar, depurar y ejecutar aplicaciones ASP.NET Core incluidas en un contenedor
para .NET Core. Se admiten contenedores de Windows y Linux.
Vea o descargue el código de ejemplo (cómo descargarlo)

Requisitos previos
Docker para Windows
Visual Studio 2017 con la carga de trabajo Desarrollo multiplataforma de .NET Core

Instalación y configuración
Para instalar Docker, primero 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). A continuación, 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 de la bandeja del sistema, y
seleccione Configuración y Unidades compartidas. Seleccione la unidad donde los archivos se almacenan en
Docker. Haga clic en Aplicar.

TIP
Las versiones 15.6 y posteriores de Visual Studio 2017 le avisan si las unidades compartidas no están configuradas.

Agregar un proyecto a un contenedor de Docker


Para incluir un proyecto de ASP.NET Core en un contenedor, el proyecto debe ser para .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
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.
Visual Studio Tools para Docker no admite la adición de Docker a un proyecto de ASP.NET Core existente para
.NET Framework.

Información general sobre Dockerfile


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
en concreto usa una compilación de varias fases, con cuatro fases de compilación distintas y cada una con un
nombre asignado:
FROM microsoft/dotnet:2.1-aspnetcore-runtime AS base
WORKDIR /app
EXPOSE 59518
EXPOSE 44364

FROM microsoft/dotnet:2.1-sdk AS build


WORKDIR /src
COPY HelloDockerTools/HelloDockerTools.csproj HelloDockerTools/
RUN dotnet restore HelloDockerTools/HelloDockerTools.csproj
COPY . .
WORKDIR /src/HelloDockerTools
RUN dotnet build HelloDockerTools.csproj -c Release -o /app

FROM build AS publish


RUN dotnet publish HelloDockerTools.csproj -c Release -o /app

FROM base AS final


WORKDIR /app
COPY --from=publish /app .
ENTRYPOINT ["dotnet", "HelloDockerTools.dll"]

El Dockerfile anterior se basa en la imagen microsoft/dotnet. Esta imagen base incluye el entorno de ejecución de
ASP.NET Core y los paquetes NuGet. Los paquetes están compilados Just-In-Time (JIT) para mejorar el
rendimiento de inicio.
Si la casilla Configurar para HTTPS del cuadro de diálogo del nuevo proyecto está marcada, el Dockerfile
expondrá dos puertos. Uno se utiliza para el tráfico HTTP, mientras que el otro se emplea para HTTPS. Si la casilla
no está marcada, se expondrá un único puerto (80) para el tráfico HTTP.

FROM microsoft/aspnetcore:2.0 AS base


WORKDIR /app
EXPOSE 80

FROM microsoft/aspnetcore-build:2.0 AS build


WORKDIR /src
COPY HelloDockerTools/HelloDockerTools.csproj HelloDockerTools/
RUN dotnet restore HelloDockerTools/HelloDockerTools.csproj
COPY . .
WORKDIR /src/HelloDockerTools
RUN dotnet build HelloDockerTools.csproj -c Release -o /app

FROM build AS publish


RUN dotnet publish HelloDockerTools.csproj -c Release -o /app

FROM base AS final


WORKDIR /app
COPY --from=publish /app .
ENTRYPOINT ["dotnet", "HelloDockerTools.dll"]

El Dockerfile anterior se basa en la imagen microsoft/aspnetcore. Esta imagen base incluye los paquetes NuGet de
ASP.NET Core, compilados Just-In-Time (JIT) para mejorar el rendimiento de inicio.

Agregar compatibilidad con un orquestador de contenedores a una


aplicación
Visual Studio 2017, versiones 15.7 o anteriores, es compatible con Docker Compose como única solución de
orquestación de contenedores. Los artefactos de Docker Compose se agregan mediante Agregar >
Compatibilidad con Docker.
Visual Studio 2017, versiones 15.8 o posteriores, permite agregar una solución de orquestación de forma manual.
Haga clic con el botón derecho en el Explorador de soluciones y seleccione Agregar > Compatibilidad con el
orquestador de contenedores. Hay dos opciones: Docker Compose y Service Fabric.
Docker Compose
Visual Studio Tools para Docker permite agregar un proyecto docker-compose a la solución con los archivos
siguientes:
docker-compose.dcproj: archivo que representa el proyecto. Incluye un elemento <DockerTargetOS> en el que se
especifica el sistema operativo que se utilizará.
.dockerignore: contiene una lista de los patrones de archivos y directorios que se excluirán al generar un
contexto de compilación.
docker-compose.yml: archivo base de Docker Compose que se utiliza para definir la colección de imágenes
compilada y ejecutada con docker-compose build y docker-compose run , respectivamente.
docker-compose.override.yml: archivo opcional que Docker Compose lee y 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.

El archivo docker-compose.yml hace referencia al nombre de la imagen que se crea al ejecutar el proyecto:

version: '3.4'

services:
hellodockertools:
image: ${DOCKER_REGISTRY}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 tiene previsto colocar la imagen en el Registro, utilice el nombre de usuario de Docker Hub como prefijo, antes
del nombre de imagen, 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.

Service Fabric
Además de los requisitos previos base, la solución de orquestación de Service Fabric presenta los siguientes
requisitos previos:
SDK de Microsoft Azure Service Fabric, versión 2.6 o posterior
Carga de trabajo Desarrollo de Azure de Visual Studio 2017
Service Fabric no admite la ejecución de contenedores de Linux en el clúster de desarrollo local de Windows. Si el
proyecto ya utiliza un contenedor de Linux, Visual Studio le solicitará que cambie a los contenedores de Windows.
Visual Studio Tools para Docker permite realizar las siguientes tareas:
Agregar un proyecto Aplicación de Service Fabric <nombre_proyecto>Aplicación a la solución.
Agregar un Dockerfile y un archivo .dockerignore al proyecto de ASP.NET Core. Si el proyecto de ASP.NET
Core ya contiene un Dockerfile, se le cambiará el nombre a Dockerfile.original. A continuación, se creará un
nuevo Dockerfile similar al siguiente:
# See https://aka.ms/containerimagehelp for information on how to use Windows Server 1709 containers
with Service Fabric.
# FROM microsoft/aspnetcore:2.0-nanoserver-1709
FROM microsoft/aspnetcore:2.0-nanoserver-sac2016
ARG source
WORKDIR /app
COPY ${source:-obj/Docker/publish} .
ENTRYPOINT ["dotnet", "HelloDockerTools.dll"]

Agregar un elemento <IsServiceFabricServiceProject> al archivo .csproj al proyecto de ASP.NET Core:

<IsServiceFabricServiceProject>True</IsServiceFabricServiceProject>

Agregar una carpeta PackageRoot al proyecto de ASP.NET Core. La carpeta incluirá un manifiesto de
servicio y las opciones de configuración del nuevo servicio.
Para obtener más información, consulte Implementación de una aplicación .NET de un contenedor de Windows en
Azure Service Fabric.

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:
Si todavía no está en la caché, se adquirirá la etiqueta 2.1 -aspnetcore-runtime de la imagen del entorno de
ejecución microsoft/dotnet. La imagen instala los entornos de ejecución de ASP.NET Core y .NET Core, así
como las bibliotecas asociadas. Además, está optimizada para ejecutar aplicaciones ASP.NET Core en
producción.
Dentro del contenedor, la variable de entorno ASPNETCORE_ENVIRONMENT se establece en Development .
Se exponen dos puertos asignados de forma dinámica: uno para HTTP y otro para HTTPS. El puerto asignado
al localhost se puede asignar mediante 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.
Seguidamente, se aplica la etiqueta dev a la imagen de Docker resultante de la aplicación. La imagen se basa en la
etiqueta 2.1 -aspnetcore-runtime de la imagen base microsoft/dotnet. 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 dev d72ce0f1dfe7 30 seconds ago 255MB
microsoft/dotnet 2.1-aspnetcore-runtime fcc3887985bb 6 days ago 255MB

Se adquiere la imagen microsoft/aspnetcore en tiempo de ejecución (si todavía no está en la caché).


Dentro del contenedor, la variable de entorno ASPNETCORE_ENVIRONMENT se establece en Development .
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.
Seguidamente, se aplica la etiqueta dev a la imagen de Docker resultante de la aplicación. La imagen se basa en la
imagen base microsoft/aspnetcore. 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 dev 5fafe5d1ad5b 4 minutes ago 347MB
microsoft/aspnetcore 2.0 c69d39472da9 13 days ago 347MB

NOTE
La imagen dev carece del contenido de la aplicación, ya que las opciones de configuración de depuración utilizan el montaje
de volúmenes 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 los archivos de código requieren compilación y un reinicio del Kestrel dentro del contenedor.
Después de realizar la modificación, use 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. Si todavía no está en la caché, las herramientas obtendrán la imagen de compilación o
publicación a partir de Docker Hub. Se generará una imagen con la etiqueta más reciente que se puede colocar en
el Registro privado o Docker Hub.
Para consultar la lista de imágenes, ejecute el comando docker images en PMC. Esto genera una salida similar a la
siguiente:

REPOSITORY TAG IMAGE ID CREATED SIZE


hellodockertools latest e3984a64230c About a minute ago 258MB
hellodockertools dev d72ce0f1dfe7 4 minutes ago 255MB
microsoft/dotnet 2.1-sdk 9e243db15f91 6 days ago 1.7GB
microsoft/dotnet 2.1-aspnetcore-runtime fcc3887985bb 6 days ago 255MB
REPOSITORY TAG IMAGE ID CREATED SIZE
hellodockertools latest cd28f0d4abbd 12 seconds ago 349MB
hellodockertools dev 5fafe5d1ad5b 23 minutes ago 347MB
microsoft/aspnetcore-build 2.0 7fed40fbb647 13 days ago 2.02GB
microsoft/aspnetcore 2.0 c69d39472da9 13 days ago 347MB

A partir de .NET Core 2.1, las imágenes microsoft/aspnetcore-build y microsoft/aspnetcore que figuran en la
salida anterior se reemplazan por imágenes microsoft/dotnet . Para obtener más información, consulte el anuncio
sobre la migración de los repositorios de Docker.

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.

Recursos adicionales
Azure Service Fabric: Preparación del entorno de desarrollo en Windows
Implementación de una aplicación .NET de un contenedor de Windows en Azure Service Fabric
Solución de problemas de desarrollo de Visual Studio 2017 con Docker
Visual Studio Tools para Docker con ASP.NET Core
Configuración de ASP.NET Core para trabajar con
servidores proxy y equilibradores de carga
12/09/2018 • 25 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
geolocalización del cliente.

Encabezados reenviados
Por costumbre, los servidores proxy reenvían la información en encabezados HTTP.

HEADER DESCRIPCIÓN

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 .
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.
Los encabezados reenviados se denominan X-Forwarded-For y X-Forwarded-Proto .
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. Si el dispositivo
usa nombres de encabezado distintos a X-Forwarded-For y X-Forwarded-Proto , establezca las opciones
ForwardedForHeaderName y ForwardedProtoHeaderName para que coincidan con los nombres de
encabezado empleados por el dispositivo. Para obtener más información, vea Opciones del Middleware de
encabezados reenviados y Configuración de un proxy que usa otros nombres de encabezado.

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.

Configuración de Nginx
Para reenviar los encabezados X-Forwarded-For y X-Forwarded-Proto , vea Hospedar ASP.NET Core en
Linux con Nginx. Para más información, vea NGINX: Using the Forwarded header (NGINX: Uso del
encabezado Forwarded).

Configuración de Apache
X-Forwarded-For se agrega automáticamente. Vea Apache Module mod_proxy: Reverse Proxy Request
Headers (Módulo de Apache mod_proxy: Encabezados de solicitud de proxy inverso). Para obtener
información sobre cómo reenviar el encabezado X-Forwarded-Proto , vea Hospedar ASP.NET Core en Linux
con Apache.

Opciones del Middleware de encabezados reenviados


ForwardedHeadersOptions controla el comportamiento del middleware de encabezados reenviados. En el
ejemplo siguiente se cambian los valores predeterminados:
Limite el número de entradas de los encabezados reenviados a 2 .
Agregue una dirección de proxy conocida de 127.0.10.1 .
Cambie el nombre del encabezado reenviado del valor predeterminado X-Forwarded-For a
X-Forwarded-For-My-Custom-Header-Name .

services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardLimit = 2;
options.KnownProxies.Add(IPAddress.Parse("127.0.10.1"));
options.ForwardedForHeaderName = "X-Forwarded-For-My-Custom-Header-Name";
});

OPCIÓN DESCRIPCIÓN

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.
Esta opción se usa cuando el reenviador o proxy no
emplea el encabezado X-Forwarded-For sino algún otro
para reenviar la información.

De manera predeterminada, es X-Forwarded-For .

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.


OPCIÓN DESCRIPCIÓN

ForwardedHostHeaderName Use el encabezado especificado por esta propiedad en


lugar del especificado por
ForwardedHeadersDefaults.XForwardedHostHeaderName.
Esta opción se usa cuando el reenviador o proxy no
emplea el encabezado X-Forwarded-Host sino algún otro
para reenviar la información.

De manera predeterminada, es X-Forwarded-Host .

ForwardedProtoHeaderName Use el encabezado especificado por esta propiedad en


lugar del especificado por
ForwardedHeadersDefaults.XForwardedProtoHeaderName.
Esta opción se usa cuando el reenviador o proxy no
emplea el encabezado X-Forwarded-Proto sino algún
otro para reenviar la información.

De manera predeterminada, 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 redes conocidas de los que se


aceptan encabezados reenviados. Proporcione intervalos
de direcciones IP mediante la notación de Enrutamiento de
interdominios sin clases (CIDR).

Si el servidor usa sockets en modo dual, las direcciones


IPv4 se suministran en formato IPv6 (por ejemplo,
10.0.0.1 en IPv4 se representa en IPv6 como
::ffff:10.0.0.1 ). Consulte IPAddress.MapToIPv6. Para
determinar si este formato es necesario, examine
HttpContext.Connection.RemoteIpAddress. Para más
información, consulte la sección Configuración de una
dirección IPv4 representada como una dirección IPv6.

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.

Si el servidor usa sockets en modo dual, las direcciones


IPv4 se suministran en formato IPv6 (por ejemplo,
10.0.0.1 en IPv4 se representa en IPv6 como
::ffff:10.0.0.1 ). Consulte IPAddress.MapToIPv6. Para
determinar si este formato es necesario, examine
HttpContext.Connection.RemoteIpAddress. Para más
información, consulte la sección Configuración de una
dirección IPv4 representada como una dirección IPv6.

El valor predeterminado es IList<IPAddress> que contiene


una única entrada para IPAddress.IPv6Loopback .
OPCIÓN DESCRIPCIÓN

OriginalForHeaderName Use el encabezado especificado por esta propiedad en


lugar del especificado por
ForwardedHeadersDefaults.XOriginalForHeaderName.

De manera predeterminada, es X-Original-For .

OriginalHostHeaderName Use el encabezado especificado por esta propiedad en


lugar del especificado por
ForwardedHeadersDefaults.XOriginalForHeaderName.

De manera predeterminada, es X-Original-Host .

OriginalProtoHeaderName Use el encabezado especificado por esta propiedad en


lugar del especificado por
ForwardedHeadersDefaults.XOriginalProtoHeaderName.

De manera predeterminada, 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 DESCRIPCIÓN

ForwardedForHeaderName Use el encabezado especificado por esta propiedad en


lugar del especificado por
ForwardedHeadersDefaults.XForwardedForHeaderName.
Esta opción se usa cuando el reenviador o proxy no
emplea el encabezado X-Forwarded-For sino algún otro
para reenviar la información.

De manera predeterminada, es X-Forwarded-For .

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.
Esta opción se usa cuando el reenviador o proxy no
emplea el encabezado X-Forwarded-Host sino algún otro
para reenviar la información.

De manera predeterminada, es X-Forwarded-Host .


OPCIÓN DESCRIPCIÓN

ForwardedProtoHeaderName Use el encabezado especificado por esta propiedad en


lugar del especificado por
ForwardedHeadersDefaults.XForwardedProtoHeaderName.
Esta opción se usa cuando el reenviador o proxy no
emplea el encabezado X-Forwarded-Proto sino algún
otro para reenviar la información.

De manera predeterminada, 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 redes conocidas de los que se


aceptan encabezados reenviados. Proporcione intervalos
de direcciones IP mediante la notación de Enrutamiento de
interdominios sin clases (CIDR).

Si el servidor usa sockets en modo dual, las direcciones


IPv4 se suministran en formato IPv6 (por ejemplo,
10.0.0.1 en IPv4 se representa en IPv6 como
::ffff:10.0.0.1 ). Consulte IPAddress.MapToIPv6. Para
determinar si este formato es necesario, examine
HttpContext.Connection.RemoteIpAddress. Para más
información, consulte la sección Configuración de una
dirección IPv4 representada como una dirección IPv6.

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.

Si el servidor usa sockets en modo dual, las direcciones


IPv4 se suministran en formato IPv6 (por ejemplo,
10.0.0.1 en IPv4 se representa en IPv6 como
::ffff:10.0.0.1 ). Consulte IPAddress.MapToIPv6. Para
determinar si este formato es necesario, examine
HttpContext.Connection.RemoteIpAddress. Para más
información, consulte la sección Configuración de una
dirección IPv4 representada como una dirección IPv6.

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.

De manera predeterminada, es X-Original-For .


OPCIÓN DESCRIPCIÓN

OriginalHostHeaderName Use el encabezado especificado por esta propiedad en


lugar del especificado por
ForwardedHeadersDefaults.XOriginalForHeaderName.

De manera predeterminada, es X-Original-Host .

OriginalProtoHeaderName Use el encabezado especificado por esta propiedad en


lugar del especificado por
ForwardedHeadersDefaults.XOriginalProtoHeaderName.

De manera predeterminada, 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 obtener más información sobre el procesamiento de pedidos del
middleware, vea Middleware de ASP.NET Core.
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();
});

Configuración de un proxy que usa otros nombres de encabezado


Si el proxy no usa los encabezados denominados X-Forwarded-For y X-Forwarded-Proto para reenviar el
puerto o la dirección de proxy y originar información de esquema, establezca las opciones
ForwardedForHeaderName y ForwardedProtoHeaderName de modo que coincidan con los nombres de
encabezado empleados por el proxy:

services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedForHeaderName = "Header_Name_Used_By_Proxy_For_X-Forwarded-For_Header";
options.ForwardedProtoHeaderName = "Header_Name_Used_By_Proxy_For_X-Forwarded-Proto_Header";
});

Configuración de una dirección IPv4 representada como una dirección IPv6


Si el servidor usa sockets en modo dual, las direcciones IPv4 se suministran en formato IPv6 (por ejemplo,
10.0.0.1 en IPv4 se representa en IPv6 como ::ffff:10.0.0.1 o ::ffff:a00:1 ). Consulte
IPAddress.MapToIPv6. Para determinar si este formato es necesario, examine
HttpContext.Connection.RemoteIpAddress.
En el ejemplo siguiente, se agrega una dirección de red que proporciona encabezados reenviados a la lista
KnownNetworks en formato IPv6.

Dirección IPv4: 10.11.12.1/8

Dirección IPv6 convertida: ::ffff:10.11.12.1


Longitud de prefijo convertida: 104
También puede proporcionar la dirección en formato hexadecimal ( 10.11.12.1 se representa en IPv6 como
::ffff:0a0b:0c01 ). Al convertir una dirección IPv4 en IPv6, agregue 96 a la longitud de prefijo de CIDR ( 8
en el ejemplo) para tener en cuenta el prefijo de IPv6 ::ffff: adicional (8 + 96 = 104).
// To access IPNetwork and IPAddress, add the following namespaces:
// using using System.Net;
// using Microsoft.AspNetCore.HttpOverrides;
services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders =
ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
options.KnownNetworks.Add(new IPNetwork(
IPAddress.Parse("::ffff:10.11.12.1"), 104));
});

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 que se procese X-Forwarded-For . Esto limita la suplantación de encabezados al no
aceptarse reenviadores de servidores proxy que no son de confianza.

Recursos adicionales
Hospedaje de ASP.NET Core en una granja de servidores web
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)
Hospedaje de ASP.NET Core en una granja de
servidores web
27/07/2018 • 10 minutes to read • Edit Online

Por Luke Latham y Chris Ross


Una granja de servidores web es un grupo de dos o más servidores web (o nodos) que hospedan varias
instancias de una aplicación. Cuando llegan solicitudes de usuarios a una granja de servidores web, un
equilibrador de carga las distribuye a los nodos de la granja de servidores web. Las granjas de servidores web
mejoran lo siguiente:
Confiabilidad y disponibilidad – Cuando se produce un error en uno o más nodos, el equilibrador de
carga puede enrutar las solicitudes a otros nodos en funcionamiento para continuar procesando las
solicitudes.
Capacidad y rendimiento – Varios nodos pueden procesar más solicitudes que un solo servidor. El
equilibrador de carga equilibra la carga de trabajo mediante la distribución de las solicitudes a los distintos
nodos.
Escalabilidad – Cuando se requiere más o menos capacidad, es posible aumentar o disminuir la cantidad de
nodos activos para coincidir con la carga de trabajo. Las tecnologías de plataforma de granjas de servidores
web, como Azure App Service, pueden agregar o quitar nodos de manera automática si lo solicita el
administrador del sistema o sin intervención humana.
Mantenimiento – Los nodos de una granja de servidores web se pueden basar en un conjunto de servicios
compartidos, lo que facilita la administración del sistema. Por ejemplo, los nodos de una granja de servidores
web pueden basarse en un servidor de base de datos única y una ubicación de red común para los recursos
estáticos, como imágenes y archivos descargables.
En este tema se describe la configuración y las dependencias de las aplicaciones de ASP.NET Core hospedadas
en una granja de servidores web que se basan en los recursos compartidos.

Configuración general
Hospedaje e implementación de ASP.NET Core
Obtenga información sobre cómo configurar entornos de hospedaje e implementar aplicaciones de ASP.NET
Core. Configure un administrador de procesos en cada nodo de la granja de servidores web para automatizar
los inicios y reinicios de la aplicación. Cada nodo requiere el tiempo de ejecución de ASP.NET Core. Para más
información, consulte los temas que aparecen en el área Hospedaje e implementación de la documentación.
Configuración de ASP.NET Core para trabajar con servidores proxy y equilibradores de carga
Aprenda sobre la configuración de las aplicaciones hospedadas detrás de los servidores proxy y equilibradores
de carga, que con frecuencia ocultan información importante sobre las solicitudes.
Hospedaje de ASP.NET Core en Azure App Service
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. App Service es una plataforma completamente administrada que brinda
escalado automático, equilibrio de carga, aplicación de revisiones e implementación continua.

Datos de la aplicación
Cuando una aplicación se escala a varias instancias, puede haber un estado de la aplicación que requiera el uso
compartido entre los distintos nodos. Si el estado es transitorio, considere el uso compartido de
IDistributedCache. Si es necesario mantener el estado compartido, considere la posibilidad de almacenar el
estado compartido en una base de datos.

Configuración necesaria
La protección de datos y el almacenamiento en caché requieren la configuración de las aplicaciones
implementadas en una granja de servidores web.
Protección de datos
Las aplicaciones usan el sistema de protección de datos de ASP.NET Core para proteger los datos. La protección
de datos se basa en un conjunto de claves criptográficas almacenadas en un conjunto de claves. Cuando se
inicializa el sistema de protección de datos, aplica la configuración predeterminada que almacena localmente el
conjunto de claves. Según la configuración predeterminada, un conjunto de claves único se almacena en cada
nodo de la granja de servidores web. Por tanto, un nodo de granja de servidores web no puede cifrar los datos
que una aplicación cifra en otro nodo. Por lo general, la configuración predeterminada no es adecuada para
hospedar aplicaciones en una granja de servidores web. Una alternativa a la implementación de un conjunto de
claves compartido es enrutar siempre las solicitudes de usuario al mismo nodo. Para más información sobre la
configuración del sistema de protección de datos para las implementaciones de granjas de servidores web,
consulte Configurar la protección de datos de ASP.NET Core.
Almacenamiento en memoria caché
En un entorno de granja de servidores web, el mecanismo de almacenamiento en caché debe compartir
elementos en caché en todos los nodos de la granja de servidores web. El almacenamiento en caché debe
basarse en una instancia común de Redis Cache, en una base de datos SQL Server compartida o en una
implementación de almacenamiento en caché personalizada que comparte los elementos en caché en toda la
granja de servidores web. Para obtener más información, vea Trabajar con una memoria caché distribuida en
ASP.NET Core.

Componentes dependientes
Los escenarios siguientes no requieren configuración adicional, pero dependen de tecnologías que sí requiere
configuración para las granjas de servidores web.

ESCENARIO DEPENDE DE …

Autenticación Protección de datos (consulte Configurar la protección de


datos de ASP.NET Core).

Para obtener más información, consulte Usar autenticación


de cookies sin ASP.NET Core Identity y Compartir cookies
entre aplicaciones con ASP.NET y ASP.NET Core.

identidad Autenticación y configuración de base de datos.

Para obtener más información, vea Introducción a la


identidad en ASP.NET Core.

Sesión Protección de datos (cookies cifradas) (consulte Configurar la


protección de datos de ASP.NET Core) y almacenamiento en
caché (consulte Trabajar con una memoria caché distribuida
en ASP.NET Core).

Para más información, consulte Estado de sesión y aplicación:


estado de sesión.
ESCENARIO DEPENDE DE …

TempData Protección de datos (cookies cifradas) (consulte Configurar la


protección de datos de ASP.NET Core) o sesión (consulte
Estado de sesión y aplicación: TempData).

Para más información, consulte Estado de sesión y aplicación:


TempData.

Antifalsificación Protección de datos (consulte Configurar la protección de


datos de ASP.NET Core).

Para obtener más información, vea Evitar Cross-Site


falsificación de solicitud entre (XSRF/CSRF) attacks en
ASP.NET Core.

Solucionar problemas
Cuando la protección de datos o el almacenamiento en caché no están configurados para un entorno de granja
de servidores web, se producen errores intermitentes cuando se procesan las solicitudes. Esto ocurre porque los
nodos no comparten los mismos recursos y las solicitudes de usuario no se enrutan siempre de vuelta al mismo
nodo.
Piense en un usuario que inicia sesión en la aplicación a través de la autenticación de cookies. El usuario inicia
sesión en la aplicación en un nodo de la granja de servidores web. Si la solicitud siguiente llega al mismo nodo
en el que iniciaron sesión, la aplicación puede descifrar la cookie de autenticación y permite el acceso al recurso
de la aplicación. Si la solicitud siguiente llega a otro nodo, la aplicación no puede descifra la cookie de
autenticación desde el nodo donde el usuario inició sesión y se produce un error en la autorización del recurso
solicitado.
Cuando cualquiera de los síntomas siguientes se producen de manera intermitente, se suele hacer un
seguimiento del problema hasta la configuración inadecuada de la protección de datos o del almacenamiento en
caché para un entorno de granja de servidores web:
La autenticación se interrumpe – La cookie de autenticación está configurada de manera incorrecta o no se
puede descifrar. Los inicios de sesión de OAuth (Facebook, Microsoft, Twitter) o de OpenIdConnect presentan
el error "Correlation failed" (Error de correlación).
La autorización se interrumpe – Se pierde la identidad.
El estado de sesión pierde datos.
Los elementos en caché desaparecen.
Error de TempData.
Error de POST – Error en la comprobación antifalsificación.
Para más información sobre la configuración de la protección de datos para las implementaciones de granjas de
servidores web, consulte Configurar la protección de datos de ASP.NET Core. Para más información sobre la
configuración del almacenamiento en caché para las implementaciones de granja de servidores web, consulte
Trabajar con una memoria caché distribuida en ASP.NET Core.
Perfiles de publicación de Visual Studio para la
implementación de aplicaciones ASP.NET Core
17/09/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.1.4" />
</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.
Publicación de una aplicación web ASP.NET en una máquina virtual de Azure desde Visual Studio
Estructura de directorios de ASP.NET Core
04/07/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 Logs de stdout se puede crear para la implementación mediante uno de los dos enfoques
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
Versiones preliminares de ASP.NET Core con Azure App Service
Las versiones preliminares de ASP.NET Core no se implementan de forma predeterminada en Azure App Service. Para
hospedar una aplicación que usa una versión preliminar de ASP.NET Core, vea 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
07/09/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 obtener más información, vea la sección de este
documento relativa a la documentación de seguridad de ASP.NET Core.

Documentación de seguridad de ASP.NET Core


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
Lista de IP seguras
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 en ASP.NET Core
18/09/2018 • 16 minutes to read • Edit Online

Por Rick Anderson


ASP.NET Core Identity es un sistema de pertenencia que agrega la funcionalidad de inicio de sesión a las
aplicaciones ASP.NET Core. Los usuarios pueden crear una cuenta con la información de inicio de sesión
almacenada en la identidad o pueden utilizar un proveedor de inicio de sesión externo. Proveedores de
inicio de sesión externos compatibles incluyen Facebook, Google, Twitter y Microsoft Account.
Identidad puede configurarse con una base de datos de SQL Server para almacenar los nombres de
usuario, contraseñas y datos de perfil. Como alternativa, otro almacén persistente puede usarse, por
ejemplo, Azure Table Storage.
Ver o descargar el código de ejemplo. (Cómo descargar)
En este tema, aprenda a usar identidad para registrar, inicie sesión y cerrar la sesión un usuario. Para
obtener instrucciones detalladas sobre cómo crear aplicaciones que usan la identidad, vea la sección pasos
siguientes al final de este artículo.

AddDefaultIdentity y AddIdentity
AddDefaultIdentity se introdujo en ASP. Core 2.1. Una llamada a AddDefaultIdentity es similar a llamar a
lo siguiente:
AddIdentity
AddDefaultUI
AddDefaultTokenProviders
Consulte AddDefaultIdentity origen para obtener más información.

Creación de una aplicación Web con autenticación


Cree un proyecto de aplicación Web ASP.NET Core con cuentas de usuario individuales.
Visual Studio
CLI de .NET Core
Seleccione Archivo > Nuevo > Proyecto.
Seleccione Aplicación web de ASP.NET Core. Denomine el proyecto WebApp1 tener el mismo
espacio de nombres como la descarga del proyecto. Haga clic en Aceptar.
Seleccione una de ASP.NET Core aplicación Web para ASP.NET Core 2.1, a continuación, seleccione
Cambiar autenticación.
Seleccione cuentas de usuario individuales y haga clic en Aceptar.
Proporciona el proyecto generado ASP.NET Core Identity como un biblioteca de clases de Razor.
Inicio de sesión y de registro de prueba
Ejecute la aplicación y registrar un usuario. Según el tamaño de pantalla, es posible que deba seleccionar el
botón de alternancia de navegación para ver el registrar y inicio de sesión vínculos.
Vista de la base de datos de identidad
Visual Studio
CLI de .NET Core
Desde el vista menú, seleccione Explorador de objetos de SQL Server (SSOX).
Vaya a MSSQLLocalDB (13 de SQL Server) (localdb). Haga doble clic en dbo. AspNetUsers > ver
datos:

Configurar servicios de identidad


Se agregan los servicios en ConfigureServices . El patrón habitual consiste en llamar a todos los métodos
Add{Service} y, luego, a todos los métodos services.Configure{Service} . El código siguiente no incluye la
plantilla genera CookiePolicyOptions :

public void ConfigureServices(IServiceCollection services)


{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<IdentityUser>()
.AddEntityFrameworkStores<ApplicationDbContext>();

services.Configure<IdentityOptions>(options =>
{
// Password settings.
options.Password.RequireDigit = true;
options.Password.RequireLowercase = true;
options.Password.RequireNonAlphanumeric = true;
options.Password.RequireUppercase = true;
options.Password.RequiredLength = 6;
options.Password.RequiredUniqueChars = 1;

// Lockout settings.
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
options.Lockout.MaxFailedAccessAttempts = 5;
options.Lockout.AllowedForNewUsers = true;

// User settings.
options.User.AllowedUserNameCharacters =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+";
options.User.RequireUniqueEmail = false;
});

services.ConfigureApplicationCookie(options =>
{
// Cookie settings
options.Cookie.HttpOnly = true;
options.ExpireTimeSpan = TimeSpan.FromMinutes(5);

options.LoginPath = "/Identity/Account/Login";
options.AccessDeniedPath = "/Identity/Account/AccessDenied";
options.SlidingExpiration = true;
});

services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}

El código anterior configura la identidad con valores de la opción predeterminada. Los servicios están
disponibles para la aplicación a través de inserción de dependencias.
Se habilita la identidad mediante una llamada a UseAuthentication. UseAuthentication Agrega
autenticación middleware a la canalización de solicitud.
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();
}
// 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();
}

Los servicios están disponibles para la aplicación a través de inserción de dependencias.


Se habilita la identidad de 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?}");
});
}

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

// Configure Identity
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;

// Lockout settings
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30);
options.Lockout.MaxFailedAccessAttempts = 10;

// Cookie settings
options.Cookies.ApplicationCookie.ExpireTimeSpan = TimeSpan.FromDays(150);
options.Cookies.ApplicationCookie.LoginPath = "/Account/Login";

// User settings
options.User.RequireUniqueEmail = true;
});

// Add application services.


services.AddTransient<IEmailSender, AuthMessageSender>();
services.AddTransient<ISmsSender, AuthMessageSender>();
}
Estos servicios están disponibles para la aplicación a través de inserción de dependencias.
Se habilita la identidad de la aplicación mediante una llamada a UseIdentity en el Configure método.
UseIdentity Agrega la autenticación basada en cookies middleware a la canalización de solicitud.

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

// Add external authentication middleware below. To configure them please see


https://go.microsoft.com/fwlink/?LinkID=532715

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

Para obtener más información, consulte el IdentityOptions clase y inicio de la aplicación.

Aplicar la técnica scaffolding registrar, inicio de sesión y cierre de


sesión
Siga el aplicar la técnica scaffolding en un proyecto de Razor sin autorización identidad instrucciones.
Visual Studio
CLI de .NET Core
Agregue los archivos de registro, inicio de sesión y cierre de sesión.
Examine el registro
Cuando un usuario hace clic en el registrar vínculo, el RegisterModel.OnPostAsync se invoca la acción. El
usuario se crea por CreateAsync en el _userManager objeto. _userManager se proporciona mediante la
inserción de dependencias):
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
returnUrl = returnUrl ?? Url.Content("~/");
if (ModelState.IsValid)
{
var user = new IdentityUser { 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.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();
}

Cuando un 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 mediante la inserción de dependencias):
//
// 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 que el inicio de sesión de inmediato en el
registro.
Iniciar sesión
Se muestra el formulario de inicio de sesión cuando:
El iniciarla vínculo está seleccionado.
Cuando un usuario accede a una página donde no se autentican o autorizado, se le redirigirá a la página
de inicio de sesión.
Cuando se envía el formulario de la página de inicio de sesión, el OnPostAsync se llama a la acción.
PasswordSignInAsync se llama en el _signInManager objeto (proporcionado por la inserción de
dependencias).
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
returnUrl = returnUrl ?? Url.Content("~/");

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(Input.Email,
Input.Password, Input.RememberMe, lockoutOnFailure: true);
if (result.Succeeded)
{
_logger.LogInformation("User logged in.");
return LocalRedirect(returnUrl);
}
if (result.RequiresTwoFactor)
{
return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl,
RememberMe = Input.RememberMe });
}
if (result.IsLockedOut)
{
_logger.LogWarning("User account locked out.");
return RedirectToPage("./Lockout");
}
else
{
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return Page();
}
}

// If we got this far, something failed, redisplay form


return Page();
}

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.
Se muestra el formulario de inicio de sesión cuando los usuarios seleccionan el iniciarla vincular o se le
redirigirá al tener acceso a una página que requiere autenticación. Cuando el usuario envía el formulario en
la página de inicio de sesión, el AccountController Login se llama a la acción.
El acción llamadas PasswordSignInAsync en el _signInManager objeto (proporcionado a
Login
AccountController mediante la inserción de dependencias).
//
// 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 o PageModel ) clase expone un User propiedad. Por ejemplo, User.Claims pueden
enumerarse para tomar decisiones de autorización.
Cerrar sesión
El cerrar sesión vínculo invoca el LogoutModel.OnPost acción.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;

namespace WebApp1.Areas.Identity.Pages.Account
{
[AllowAnonymous]
public class LogoutModel : PageModel
{
private readonly SignInManager<IdentityUser> _signInManager;
private readonly ILogger<LogoutModel> _logger;

public LogoutModel(SignInManager<IdentityUser> signInManager, ILogger<LogoutModel> logger)


{
_signInManager = signInManager;
_logger = logger;
}

public void OnGet()


{
}

public async Task<IActionResult> OnPost(string returnUrl = null)


{
await _signInManager.SignOutAsync();
_logger.LogInformation("User logged out.");
if (returnUrl != null)
{
return LocalRedirect(returnUrl);
}
else
{
return Page();
}
}
}
}

SignOutAsync borra las notificaciones de usuario almacenadas en una cookie. No redirigir después de
llamar a SignOutAsync o que el usuario no cerrar la sesión.
POST se especifica en el Pages/Shared/_LoginPartial.cshtml:
@using Microsoft.AspNetCore.Identity

@inject SignInManager<IdentityUser> SignInManager


@inject UserManager<IdentityUser> UserManager

@if (SignInManager.IsSignedIn(User))
{
<form asp-area="Identity" asp-page="/Account/Logout"
asp-route-returnUrl="@Url.Page("/Index", new { area = "" })"
method="post"
id="logoutForm" class="navbar-right">
<ul class="nav navbar-nav navbar-right">
<li>
<a asp-area="Identity" asp-page="/Account/Manage/Index" title="Manage">
Hello @UserManager.GetUserName(User)!</a>
</li>
<li>
<button type="submit" class="btn btn-link navbar-btn navbar-link">Logout</button>
</li>
</ul>
</form>
}
else
{
<ul class="nav navbar-nav navbar-right">
<li><a asp-area="Identity" asp-page="/Account/Register">Register</a></li>
<li><a asp-area="Identity" asp-page="/Account/Login">Login</a></li>
</ul>
}

Al hacer clic en el cerrar sesión llamadas de vincular 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 llama el _signInManager.SignOutAsync método. El SignOutAsync método borra las


notificaciones de usuario almacenadas en una cookie.

Comprobar la identidad
Las plantillas de proyecto de web predeterminada permiten accesos anónimos a las páginas principales.
Para probar la identidad, agregue [Authorize] a la página About.
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace WebApp1.Pages
{
[Authorize]
public class AboutModel : PageModel
{
public string Message { get; set; }

public void OnGet()


{
Message = "Your application description page.";
}
}
}

Si ha iniciado sesión, cerrar sesión. Ejecute la aplicación y seleccione el sobre vínculo. Se le redirigirá a la
página de inicio de sesión.
Explore la identidad
Para explorar la identidad con más detalle:
Crear origen de la interfaz de usuario de identidad completa
Examine el origen de cada página y paso a través del depurador.

Componentes de identidad
Todas la identidad dependientes paquetes de NuGet se incluyen en el Microsoft.AspNetCore.App
metapaquete.
El paquete principal de identidad está Microsoft.AspNetCore.Identity. Este paquete contiene el conjunto
principal de interfaces de ASP.NET Core Identity y se incluye de forma
Microsoft.AspNetCore.Identity.EntityFrameworkCore .

Migración a ASP.NET Core Identity


Para obtener más información e instrucciones sobre cómo migrar el almacén de identidades existente,
consulte migrar autenticación e identidad.

Configuración de seguridad de la contraseña


Consulte configuración para obtener un ejemplo que establece los requisitos de contraseña mínima.

Pasos siguientes
Configuración de Identity
Crear una aplicación ASP.NET Core con datos de usuario protegidos por autorización
Agregar, descargar y eliminar datos de usuario personalizado a la identidad en un proyecto de ASP.NET
Core
Habilitar la generación de código QR para aplicaciones de authenticator TOTP en ASP.NET Core
Configurar el tipo de datos de las claves principales de identidad.
Migrar de autenticación e identidad a ASP.NET Core
Confirmación de la cuenta y la recuperación de contraseñas en ASP.NET Core
Autenticación en dos fases con SMS en ASP.NET Core
Hospedaje de ASP.NET Core en una granja de servidores web
Identidad de scaffold en proyectos de ASP.NET Core
21/09/2018 • 23 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 identidad pueden aplicar el proveedor de scaffolding para agregar de
forma selectiva el código fuente contenido en la biblioteca de clase de Razor de identidad (RCL ). Puede que
quiera generar código fuente que le permita modificar un código y cambiar el comportamiento; así, por ejemplo,
podría indicar al proveedor de scaffolding que generara el código que se usa en el registro. Dicho código
generado tendrá prioridad sobre el mismo código en el RCL de Identity. Para obtener el control completo de la
interfaz de usuario y no utilice el valor predeterminado RCL, consulte la sección crear origen de la interfaz de
usuario de identidad completa.
Aplicaciones que hacen lo no incluyen autenticación puede aplicar el proveedor de scaffolding para agregar el
paquete RCL identidad. Existe la posibilidad de seleccionar el código de Identity que se va a generar.
Aunque el proveedor de scaffolding genera la mayor parte del código necesario, tendrá que actualizar el
proyecto para completar el proceso. Este documento explica los pasos necesarios para completar una
actualización de scaffolding de identidad.
Cuando se ejecuta el proveedor de scaffolding 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 scaffolding de identidad. Este documento contiene instrucciones más
completas que la ScaffoldingReadme.txt archivo.
Se recomienda usar un sistema de control de código fuente que se muestra las diferencias de archivo y le
permite revertir los cambios. Inspeccione los cambios después de ejecutar el proveedor de scaffolding de
identidad.

NOTE
Los servicios son necesarios para usar autenticación en dos fases, confirmación y la contraseña de recuperación de la
cuentay otras características de seguridad con la identidad. Códigos auxiliares de servicio o servicios no se generan
cuando el scaffolding de identidad. Servicios para habilitar estas características deben agregarse manualmente. Por
ejemplo, vea requerir confirmación por correo electrónico.

Identidad de scaffold en un proyecto vacío


Ejecute el proveedor de scaffolding de identidad:
Visual Studio
CLI de .NET Core
Desde el Explorador de soluciones, haga doble clic en el proyecto > agregar > nuevo elemento de
scaffolding.
En el panel izquierdo de la agregar Scaffold cuadro de diálogo, seleccione identidad > agregar.
En el identidad de ADD 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. Por ejemplo ~/Pages/Shared/_Layout.cshtml para las páginas de Razor
~/Views/Shared/_Layout.cshtml para los proyectos de MVC
Seleccione el + botón para crear un nuevo clase de contexto de datos.
Seleccione agregar.
Agregue las siguientes llamadas resaltadas para el 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 se recomienda, aunque no es necesario. Consulte protocolo de seguridad de transporte estricto HTTP
para obtener más información.
Requiere el código de base de datos de identidad generado migraciones de Entity Framework Core. Cree una
migración y actualización de la base de datos. Por ejemplo, ejecute los siguientes comandos:
Visual Studio
CLI de .NET Core
En Visual Studio Package Manager Console:

Add-Migration CreateIdentitySchema
Update-Database

El parámetro de nombre "CreateIdentitySchema" para el Add-Migration comando es arbitrario.


"CreateIdentitySchema" Describe la migración.

Identidad de scaffold en un proyecto de Razor sin autorización


existente
Ejecute el proveedor de scaffolding de identidad:
Visual Studio
CLI de .NET Core
Desde el Explorador de soluciones, haga doble clic en el proyecto > agregar > nuevo elemento de
scaffolding.
En el panel izquierdo de la agregar Scaffold cuadro de diálogo, seleccione identidad > agregar.
En el identidad de ADD 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. Por ejemplo ~/Pages/Shared/_Layout.cshtml para las páginas de Razor
~/Views/Shared/_Layout.cshtml para los proyectos de MVC
Seleccione el + botón para crear un nuevo clase de contexto de datos.
Seleccione agregar.
Identidad se ha configurado en Areas/Identity/IdentityHostingStartup.cs. Para obtener más información,
consulte IHostingStartup.
Las migraciones, UseAuthentication y diseño
Requiere el código de base de datos de identidad generado migraciones de Entity Framework Core. Cree una
migración y actualización de la base de datos. Por ejemplo, ejecute los siguientes comandos:
Visual Studio
CLI de .NET Core
En Visual Studio Package Manager Console:

Add-Migration CreateIdentitySchema
Update-Database

El parámetro de nombre "CreateIdentitySchema" para el Add-Migration comando es arbitrario.


"CreateIdentitySchema" Describe la migración.

Habilitar la autenticación
En el Configure método de la Startup class, 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 se recomienda, aunque no es necesario. Consulte protocolo de seguridad de transporte estricto HTTP
para obtener más información.
Cambios de diseño
Opcional: Agregue 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 src="~/js/site.min.js" asp-append-version="true"></script>
</environment>

@RenderSection("Scripts", required: false)


</body>
</html>

Identidad de scaffold en un proyecto de Razor sin autorización


Ejecute el proveedor de scaffolding de identidad:
Visual Studio
CLI de .NET Core
Desde el Explorador de soluciones, haga doble clic en el proyecto > agregar > nuevo elemento de
scaffolding.
En el panel izquierdo de la agregar Scaffold cuadro de diálogo, seleccione identidad > agregar.
En el identidad de ADD 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 de 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, cree una clase de usuario personalizada para la
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 scaffold en un proyecto MVC sin autorización existente


Ejecute el proveedor de scaffolding de identidad:
Visual Studio
CLI de .NET Core
Desde el Explorador de soluciones, haga doble clic en el proyecto > agregar > nuevo elemento de
scaffolding.
En el panel izquierdo de la agregar Scaffold cuadro de diálogo, seleccione identidad > agregar.
En el identidad de ADD 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. Por ejemplo ~/Pages/Shared/_Layout.cshtml para las páginas de Razor
~/Views/Shared/_Layout.cshtml para los proyectos de MVC
Seleccione el + botón para crear un nuevo clase de contexto de datos.
Seleccione agregar.
Opcional: Agregue 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"
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 el código de base de datos de identidad generado migraciones de Entity Framework Core. Cree una
migración y actualización de la base de datos. Por ejemplo, ejecute los siguientes comandos:
Visual Studio
CLI de .NET Core
En Visual Studio Package Manager Console:

Add-Migration CreateIdentitySchema
Update-Database

El parámetro de nombre "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 se recomienda, aunque no es necesario. Consulte protocolo de seguridad de transporte estricto HTTP
para obtener más información.

Identidad de scaffold en un proyecto MVC con autorización


Ejecute el proveedor de scaffolding de identidad:
Visual Studio
CLI de .NET Core
Desde el Explorador de soluciones, haga doble clic en el proyecto > agregar > nuevo elemento de
scaffolding.
En el panel izquierdo de la agregar Scaffold cuadro de diálogo, seleccione identidad > agregar.
En el identidad de ADD 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 de 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, cree una clase de usuario personalizada para la
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 en 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 proveedor de scaffolding de
identidad y seleccione reemplazar todos los archivos.
El código resaltado siguiente muestra los cambios para reemplazar el valor predeterminado de la interfaz de
usuario de identidad con la identidad en una aplicación web de ASP.NET Core 2.1. Es posible que desee hacer
esto para tener control total sobre 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>()
.AddDefaultTokenProviders();

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>()
.AddDefaultTokenProviders();

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

public class EmailSender : IEmailSender


{
public Task SendEmailAsync(string email, string subject, string message)
{
return Task.CompletedTask;
}
}

Recursos adicionales
Cambios en el código de autenticación en ASP.NET Core 2.1 y versiones posteriores
Agregar, descargar y eliminar datos de usuario
personalizado a la identidad en un proyecto de
ASP.NET Core
21/09/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 ASP.NET Core.
Incorpore al modelo de datos de usuario personalizado con el PersonalData atributo para que esté disponible
automáticamente para la descarga y eliminación. Hacer que los datos que puede descargarse y eliminado
ayuda a cumplir RGPD requisitos.
Se crea el proyecto de ejemplo desde una aplicación web de páginas de Razor, pero las instrucciones son similares
para una aplicación web de ASP.NET Core MVC.
Vea o descargue el código de ejemplo (cómo descargarlo)

Requisitos previos
SDK de .NET Core 2.1 o versiones posteriores

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 los Descargar ejemplo código.
Seleccione aplicación Web 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 el proveedor de scaffolding de identidad


Visual Studio
CLI de .NET Core
Desde el Explorador de soluciones, haga doble clic en el proyecto > agregar > nuevo elemento de
scaffolding.
En el panel izquierdo de la agregar Scaffold cuadro de diálogo, seleccione identidad > agregar.
En el identidad de ADD 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 o índice
Seleccione el + botón para crear un nuevo clase de contexto de datos. Acepte el tipo
(WebApp1.Models.WebApp1Context si el proyecto se denomina WebApp1).
Seleccione el + botón para crear un nuevo clase User. Acepte el tipo (WebApp1User si el proyecto se
denomina WebApp1) > agregar.
Seleccione agregar.
Siga las instrucciones migraciones, UseAuthentication y diseño para realizar los pasos siguientes:
Cree una migración y actualización de 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). Es posible que deba 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 derivados de la clase con propiedades personalizadas. Si el nombre del 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 decoran con el PersonalData atributo son:


Cuando elimina el Areas/Identity/Pages/Account/Manage/DeletePersonalData.cshtml llama la página de
Razor UserManager.Delete .
Incluye en los datos descargados por el
Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtml página de Razor.
Actualizar la página Account/Manage/Index.cshtml
Actualización de la InputModel en Areas/Identity/Pages/Account/Manage/Index.cshtml.cs con el siguiente código
resaltado:

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 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 con el siguiente código
resaltado:

[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.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.
Agrega una migración para los datos de usuario personalizada
Visual Studio
CLI de .NET Core
En Visual Studio Package Manager Console:
Add-Migration CustomUserData
Update-Database

Prueba crear, ver, descargar y 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 desde el /Identity/Account/Manage/PersonalData página.
Personalización del modelo de identidad en ASP.NET
Core
27/09/2018 • 30 minutes to read • Edit Online

Por Arthur Vickers


Identidad de ASP.NET Core proporciona un marco para administrar y almacenar las cuentas de usuario en
aplicaciones ASP.NET Core. Identidad se agrega al proyecto cuando cuentas de usuario individuales está
seleccionado como el mecanismo de autenticación. De forma predeterminada, identidad hace uso de Entity
Framework (EF ) modelo de datos principal. En este artículo se describe cómo personalizar el modelo de identidad.

Identidad y migraciones de EF Core


Antes de examinar el modelo, es útil comprender cómo funciona la identidad con migraciones de EF Core para
crear y actualizar una base de datos. En el nivel superior, el proceso es:
1. Definir o actualizar un modelo de datos en el código.
2. Agregue una migración para convertir este modelo en los cambios que se pueden aplicar a la base de datos.
3. Compruebe que la migración representa correctamente sus intenciones.
4. Aplique la migración para actualizar la base de datos estén sincronizadas con el modelo.
5. Repita los pasos del 1 al 4 para perfeccionar el modelo y mantener sincronizada la base de datos.
Para agregar y aplicar migraciones, use uno de los enfoques siguientes:
El Package Manager Console ventana de (PMC ) si usa Visual Studio. Para obtener más información, consulte
herramientas de EF Core PMC.
La CLI de .NET Core si usa la línea de comandos. Para obtener más información, consulte herramientas de línea
de comandos de EF Core .NET.
Al hacer clic en el aplicar migraciones botón en la página de error cuando se ejecuta la aplicación.
ASP.NET Core tiene un controlador de la página de error de tiempo de desarrollo. El controlador puede aplicar las
migraciones cuando se ejecuta la aplicación. Para las aplicaciones de producción, a menudo es más adecuado para
generar scripts SQL a partir de las migraciones e implementar los cambios de la base de datos como parte de una
implementación controlada de aplicación y base de datos.
Cuando se crea una nueva aplicación mediante la identidad, ya se han completado los pasos 1 y 2 anteriores. Es
decir, el modelo de datos iniciales ya existe y la migración inicial se ha agregado al proyecto. La migración inicial
todavía debe aplicarse a la base de datos. Puede aplicar la migración inicial a través de uno de los métodos
siguientes:
Ejecute Update-Database en la PMC.
Ejecute dotnet ef database update en un shell de comandos.
Haga clic en el aplicar migraciones botón en la página de error cuando se ejecuta la aplicación.
Repita los pasos anteriores según se realizan cambios en el modelo.

El modelo de identidad
Tipos de entidad
El modelo de identidad consta de los siguientes tipos de entidad.
TIPO DE ENTIDAD DESCRIPCIÓN

User Representa al usuario.

Role Representa un rol.

UserClaim Representa una notificación que posee un usuario.

UserToken Representa un token de autenticación para un usuario.

UserLogin Asocia un usuario a un inicio de sesión.

RoleClaim Representa una notificación que se concede a todos los


usuarios dentro de un rol.

UserRole Una entidad de combinación que se asocia a los usuarios y


roles.

Relaciones entre tipos de entidad


El tipos de entidad están relacionados entre sí de las maneras siguientes:
Cada User puede tener muchos UserClaims .
Cada User puede tener muchos UserLogins .
Cada User puede tener muchos UserTokens .
Cada Role puede tener muchos asociados RoleClaims .
Cada User puede tener muchos asociados Roles y cada Role puede asociarse con muchos Users . Se trata de
una relación de varios a varios que requiere una tabla combinada en la base de datos. La tabla de combinación
se representa mediante el UserRole entidad.
Configuración predeterminada del modelo
Identidad define muchos las clases de contexto que heredan de DbContext para configurar y usar el modelo. Esta
configuración se realiza mediante el EF Core Fluent API de Code First en el OnModelCreating método de la clase
de contexto. La configuración predeterminada es:

builder.Entity<TUser>(b =>
{
// Primary key
b.HasKey(u => u.Id);

// Indexes for "normalized" username and email, to allow efficient lookups


b.HasIndex(u => u.NormalizedUserName).HasName("UserNameIndex").IsUnique();
b.HasIndex(u => u.NormalizedEmail).HasName("EmailIndex");

// Maps to the AspNetUsers table


b.ToTable("AspNetUsers");

// A concurrency token for use with the optimistic concurrency checking


b.Property(u => u.ConcurrencyStamp).IsConcurrencyToken();

// Limit the size of columns to use efficient database types


b.Property(u => u.UserName).HasMaxLength(256);
b.Property(u => u.NormalizedUserName).HasMaxLength(256);
b.Property(u => u.Email).HasMaxLength(256);
b.Property(u => u.NormalizedEmail).HasMaxLength(256);

// The relationships between User and other entity types


// Note that these relationships are configured with no navigation properties
// Each User can have many UserClaims
b.HasMany<TUserClaim>().WithOne().HasForeignKey(uc => uc.UserId).IsRequired();

// Each User can have many UserLogins


b.HasMany<TUserLogin>().WithOne().HasForeignKey(ul => ul.UserId).IsRequired();

// Each User can have many UserTokens


b.HasMany<TUserToken>().WithOne().HasForeignKey(ut => ut.UserId).IsRequired();

// Each User can have many entries in the UserRole join table
b.HasMany<TUserRole>().WithOne().HasForeignKey(ur => ur.UserId).IsRequired();
});

builder.Entity<TUserClaim>(b =>
{
// Primary key
b.HasKey(uc => uc.Id);

// Maps to the AspNetUserClaims table


b.ToTable("AspNetUserClaims");
});

builder.Entity<TUserLogin>(b =>
{
// Composite primary key consisting of the LoginProvider and the key to use
// with that provider
b.HasKey(l => new { l.LoginProvider, l.ProviderKey });

// Limit the size of the composite key columns due to common DB restrictions
b.Property(l => l.LoginProvider).HasMaxLength(128);
b.Property(l => l.ProviderKey).HasMaxLength(128);

// Maps to the AspNetUserLogins table


b.ToTable("AspNetUserLogins");
});

builder.Entity<TUserToken>(b =>
{
// Composite primary key consisting of the UserId, LoginProvider and Name
b.HasKey(t => new { t.UserId, t.LoginProvider, t.Name });

// Limit the size of the composite key columns due to common DB restrictions
b.Property(t => t.LoginProvider).HasMaxLength(maxKeyLength);
b.Property(t => t.Name).HasMaxLength(maxKeyLength);

// Maps to the AspNetUserTokens table


b.ToTable("AspNetUserTokens");
});

builder.Entity<TRole>(b =>
{
// Primary key
b.HasKey(r => r.Id);

// Index for "normalized" role name to allow efficient lookups


b.HasIndex(r => r.NormalizedName).HasName("RoleNameIndex").IsUnique();

// Maps to the AspNetRoles table


b.ToTable("AspNetRoles");

// A concurrency token for use with the optimistic concurrency checking


b.Property(r => r.ConcurrencyStamp).IsConcurrencyToken();

// Limit the size of columns to use efficient database types


b.Property(u => u.Name).HasMaxLength(256);
b.Property(u => u.NormalizedName).HasMaxLength(256);

// The relationships between Role and other entity types


// Note that these relationships are configured with no navigation properties

// Each Role can have many entries in the UserRole join table
b.HasMany<TUserRole>().WithOne().HasForeignKey(ur => ur.RoleId).IsRequired();

// Each Role can have many associated RoleClaims


b.HasMany<TRoleClaim>().WithOne().HasForeignKey(rc => rc.RoleId).IsRequired();
});

builder.Entity<TRoleClaim>(b =>
{
// Primary key
b.HasKey(rc => rc.Id);

// Maps to the AspNetRoleClaims table


b.ToTable("AspNetRoleClaims");
});

builder.Entity<TUserRole>(b =>
{
// Primary key
b.HasKey(r => new { r.UserId, r.RoleId });

// Maps to the AspNetUserRoles table


b.ToTable("AspNetUserRoles");
});

Tipos genéricos de modelo


Identidad define el valor predeterminado Common Language Runtime tipos (CLR ) para cada uno de los tipos de
entidad mencionados anteriormente. Estos tipos tienen antepuesto el prefijo identidad:
IdentityUser
IdentityRole
IdentityUserClaim
IdentityUserToken
IdentityUserLogin
IdentityRoleClaim
IdentityUserRole

En lugar de usar directamente estos tipos, se pueden usar los tipos como clases base para los tipos de la aplicación.
El DbContext clases definidas por la identidad son genéricas, tal que se pueden usar diferentes tipos CLR para uno
o varios de los tipos de entidad en el modelo. Estos tipos genéricos también permiten la User principal (PK) datos
tipo de clave que se puede cambiar.
Cuando se usa la identidad con el soporte técnico para los roles, un IdentityDbContext se debe usar la clase. Por
ejemplo:
// Uses all the built-in Identity types
// Uses `string` as the key type
public class IdentityDbContext
: IdentityDbContext<IdentityUser, IdentityRole, string>
{
}

// Uses the built-in Identity types except with a custom User type
// Uses `string` as the key type
public class IdentityDbContext<TUser>
: IdentityDbContext<TUser, IdentityRole, string>
where TUser : IdentityUser
{
}

// Uses the built-in Identity types except with custom User and Role types
// The key type is defined by TKey
public class IdentityDbContext<TUser, TRole, TKey> : IdentityDbContext<
TUser, TRole, TKey, IdentityUserClaim<TKey>, IdentityUserRole<TKey>,
IdentityUserLogin<TKey>, IdentityRoleClaim<TKey>, IdentityUserToken<TKey>>
where TUser : IdentityUser<TKey>
where TRole : IdentityRole<TKey>
where TKey : IEquatable<TKey>
{
}

// No built-in Identity types are used; all are specified by generic arguments
// The key type is defined by TKey
public abstract class IdentityDbContext<
TUser, TRole, TKey, TUserClaim, TUserRole, TUserLogin, TRoleClaim, TUserToken>
: IdentityUserContext<TUser, TKey, TUserClaim, TUserLogin, TUserToken>
where TUser : IdentityUser<TKey>
where TRole : IdentityRole<TKey>
where TKey : IEquatable<TKey>
where TUserClaim : IdentityUserClaim<TKey>
where TUserRole : IdentityUserRole<TKey>
where TUserLogin : IdentityUserLogin<TKey>
where TRoleClaim : IdentityRoleClaim<TKey>
where TUserToken : IdentityUserToken<TKey>

También es posible usar identidad sin roles (solo notificaciones), en cuyo caso un IdentityUserContext<TUser> se
debe usar la clase:
// Uses the built-in non-role Identity types except with a custom User type
// Uses `string` as the key type
public class IdentityUserContext<TUser>
: IdentityUserContext<TUser, string>
where TUser : IdentityUser
{
}

// Uses the built-in non-role Identity types except with a custom User type
// The key type is defined by TKey
public class IdentityUserContext<TUser, TKey> : IdentityUserContext<
TUser, TKey, IdentityUserClaim<TKey>, IdentityUserLogin<TKey>,
IdentityUserToken<TKey>>
where TUser : IdentityUser<TKey>
where TKey : IEquatable<TKey>
{
}

// No built-in Identity types are used; all are specified by generic arguments, with no roles
// The key type is defined by TKey
public abstract class IdentityUserContext<
TUser, TKey, TUserClaim, TUserLogin, TUserToken> : DbContext
where TUser : IdentityUser<TKey>
where TKey : IEquatable<TKey>
where TUserClaim : IdentityUserClaim<TKey>
where TUserLogin : IdentityUserLogin<TKey>
where TUserToken : IdentityUserToken<TKey>
{
}

Personalizar el modelo
El punto de partida para la personalización del modelo consiste en derivar del tipo de contexto apropiado. Consulte
la tipos genéricos de modelo sección. Este tipo de contexto se denomina habitualmente ApplicationDbContext y se
crea mediante las plantillas de ASP.NET Core.
El contexto se utiliza para configurar el modelo de dos maneras:
Proporciona la entidad y tipos de clave para los parámetros de tipo genérico.
Reemplazar OnModelCreating para modificar la asignación de estos tipos.

Cuando se reemplaza OnModelCreating , base.OnModelCreating se debe llamar primero; la configuración de


invalidación debe llamarse a continuación. EF Core generalmente tiene una directiva de última uno-wins para la
configuración. Por ejemplo, si el ToTable para un tipo de entidad es llamar primero al método con el nombre de
una tabla y, a continuación, de nuevo más tarde con un nombre de tabla diferente, el nombre de tabla en la
segunda llamada se utiliza.
Datos de usuario personalizada
Datos de usuario personalizada se admite mediante la herencia de IdentityUser . Es habitual para este tipo de
nombre ApplicationUser :

public class ApplicationUser : IdentityUser


{
public string CustomTag { get; set; }
}

Use el ApplicationUser tipo como un argumento genérico para el contexto:


public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
}

No es necesario invalidar OnModelCreating en el ApplicationDbContext clase. EF Core se asigna el CustomTag


propiedad por convención. Sin embargo, la base de datos debe actualizarse para crear un nuevo CustomTag
columna. Para crear la columna, agregar una migración y, a continuación, actualice la base de datos, como se
describe en identidad y migraciones de EF Core.
Actualización Startup.ConfigureServices para utilizar el nuevo ApplicationUser clase:

services.AddDefaultIdentity<ApplicationUser>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultUI();

En ASP.NET Core 2.1 o posterior, la identidad se proporciona como una biblioteca de clases de Razor. Para obtener
más información, consulta Identidad de scaffold en proyectos de ASP.NET Core. Por lo tanto, el código anterior
requiere una llamada a AddDefaultUI. Si el proveedor de scaffolding de identidad se usó para agregar archivos de
identidad para el proyecto, quite la llamada a AddDefaultUI . Para obtener más información, consulte:
Identidad de scaffolding
Agregar, descargar y eliminar datos de usuario personalizada para la identidad

services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();

services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext, Guid>()
.AddDefaultTokenProviders();

Cambiar el tipo de clave principal


Un cambio en el tipo de datos de la columna de clave principal una vez creada la base de datos es problemático en
muchos sistemas de base de datos. Cambiar la clave principal normalmente implica quitar y volver a crear la tabla.
Por lo tanto, los tipos de clave deben especificarse en la migración inicial cuando se crea la base de datos.
Siga estos pasos para cambiar el tipo de clave principal:
1. Si la base de datos se creó antes del cambio de clave principal, ejecute Drop-Database (PMC ) o
dotnet ef database drop ( CLI de .NET Core) para eliminarlo.

2. Después de confirmar la eliminación de la base de datos, quite la migración inicial con Remove-Migration
(PMC ) o dotnet ef migrations remove (CLI de .NET Core).
3. Actualización de la ApplicationDbContext clase derive de IdentityDbContext<TUser,TRole,TKey>.
Especifique el nuevo tipo de clave para TKey . Por ejemplo, para usar un Guid tipo de clave:
public class ApplicationDbContext
: IdentityDbContext<IdentityUser<Guid>, IdentityRole<Guid>, Guid>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
}

En el código anterior, las clases genéricas IdentityUser<TKey> y IdentityRole<TKey> debe especificarse para
usar el nuevo tipo de clave.
En el código anterior, las clases genéricas IdentityUser<TKey> y IdentityRole<TKey> debe especificarse para
usar el nuevo tipo de clave.
Startup.ConfigureServices debe actualizarse para utilizar el usuario genérico:

services.AddDefaultIdentity<IdentityUser<Guid>>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();

services.AddIdentity<IdentityUser<Guid>, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();

services.AddIdentity<IdentityUser<Guid>, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext, Guid>()
.AddDefaultTokenProviders();

4. Si un personalizado ApplicationUser clase se utiliza, actualice la clase para heredar de IdentityUser . Por
ejemplo:

using System;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;

public class ApplicationUser : IdentityUser<Guid>


{
public string CustomTag { get; set; }
}

using System;
using Microsoft.AspNetCore.Identity;

public class ApplicationUser : IdentityUser<Guid>


{
public string CustomTag { get; set; }
}

Actualización ApplicationDbContext para hacer referencia a la personalizada ApplicationUser clase:


public class ApplicationDbContext
: IdentityDbContext<ApplicationUser, IdentityRole<Guid>, Guid>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
}

Registrar la clase de contexto de base de datos personalizados al agregar el servicio de identidad en


Startup.ConfigureServices :

services.AddDefaultIdentity<ApplicationUser>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultUI()
.AddDefaultTokenProviders();

Tipo de datos de la clave principal se deduce mediante el análisis de la DbContext objeto.


En ASP.NET Core 2.1 o posterior, la identidad se proporciona como una biblioteca de clases de Razor. Para
obtener más información, consulta Identidad de scaffold en proyectos de ASP.NET Core. Por lo tanto, el
código anterior requiere una llamada a AddDefaultUI. Si el proveedor de scaffolding de identidad se usó
para agregar archivos de identidad para el proyecto, quite la llamada a AddDefaultUI .

services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();

Tipo de datos de la clave principal se deduce mediante el análisis de la DbContext objeto.

services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext, Guid>()
.AddDefaultTokenProviders();

El AddEntityFrameworkStores método acepta un TKey tipo que indica el tipo de datos de la clave principal.
5. Si un personalizado ApplicationRole clase se utiliza, actualice la clase para heredar de IdentityRole<TKey> .
Por ejemplo:

using System;
using Microsoft.AspNetCore.Identity;

public class ApplicationRole : IdentityRole<Guid>


{
public string Description { get; set; }
}

Actualización ApplicationDbContext para hacer referencia a la personalizada ApplicationRole clase. Por


ejemplo, la clase siguiente hace referencia a una personalizada ApplicationUser y personalizada
ApplicationRole :
using System;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

public class ApplicationDbContext :


IdentityDbContext<ApplicationUser, ApplicationRole, Guid>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
}

Registrar la clase de contexto de base de datos personalizados al agregar el servicio de identidad en


Startup.ConfigureServices :

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<ApplicationUser, ApplicationRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultUI()
.AddDefaultTokenProviders();

services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}

Tipo de datos de la clave principal se deduce mediante el análisis de la DbContext objeto.


En ASP.NET Core 2.1 o posterior, la identidad se proporciona como una biblioteca de clases de Razor. Para
obtener más información, consulta Identidad de scaffold en proyectos de ASP.NET Core. Por lo tanto, el
código anterior requiere una llamada a AddDefaultUI. Si el proveedor de scaffolding de identidad se usó
para agregar archivos de identidad para el proyecto, quite la llamada a AddDefaultUI .

using System;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

public class ApplicationDbContext :


IdentityDbContext<ApplicationUser, ApplicationRole, Guid>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
}

Registrar la clase de contexto de base de datos personalizados al agregar el servicio de identidad en


Startup.ConfigureServices :
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));

services.AddIdentity<ApplicationUser, ApplicationRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();

services.AddMvc()
.AddRazorPagesOptions(options =>
{
options.Conventions.AuthorizeFolder("/Account/Manage");
options.Conventions.AuthorizePage("/Account/Logout");
});

services.AddSingleton<IEmailSender, EmailSender>();
}

Tipo de datos de la clave principal se deduce mediante el análisis de la DbContext objeto.

using System;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

public class ApplicationDbContext :


IdentityDbContext<ApplicationUser, ApplicationRole, Guid>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
}

Registrar la clase de contexto de base de datos personalizados al agregar el servicio de identidad en


Startup.ConfigureServices :

public void ConfigureServices(IServiceCollection services)


{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));

services.AddIdentity<ApplicationUser, ApplicationRole>()
.AddEntityFrameworkStores<ApplicationDbContext, Guid>()
.AddDefaultTokenProviders();

services.AddMvc();

services.AddTransient<IEmailSender, AuthMessageSender>();
services.AddTransient<ISmsSender, AuthMessageSender>();
}

El AddEntityFrameworkStores método acepta un TKey tipo que indica el tipo de datos de la clave principal.
Agregar las propiedades de navegación
Cambiar la configuración del modelo para las relaciones puede ser más difícil que realizar otros cambios. Debe
tener cuidado para reemplazar las relaciones existentes en lugar de crear nuevas relaciones adicionales. En
concreto, la relación modificada debe especificar la propiedad de clave externa misma (FK) como la relación
existente. Por ejemplo, la relación entre Users y UserClaims es, de forma predeterminada, se especifica como
sigue:

builder.Entity<TUser>(b =>
{
// Each User can have many UserClaims
b.HasMany<TUserClaim>()
.WithOne()
.HasForeignKey(uc => uc.UserId)
.IsRequired();
});

La clave externa de esta relación se especifica como el UserClaim.UserId propiedad. HasMany y WithOne se llama
sin argumentos para crear la relación sin propiedades de navegación.
Agregar una propiedad de navegación ApplicationUser que permite asociados UserClaims hacer referencia a ella
desde el usuario:

public class ApplicationUser : IdentityUser


{
public virtual ICollection<IdentityUserClaim<string>> Claims { get; set; }
}

El TKey para es del tipo especificado para la clave principal de los usuarios. En este caso,
IdentityUserClaim<TKey>
TKey es string porque se usan los valores predeterminados. Tiene no el tipo de clave principal para el
UserClaim tipo de entidad.

Ahora que existe la propiedad de navegación, deben configurarse en OnModelCreating :

public class ApplicationDbContext : IdentityDbContext<ApplicationUser>


{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}

protected override void OnModelCreating(ModelBuilder modelBuilder)


{
base.OnModelCreating(modelBuilder);

modelBuilder.Entity<ApplicationUser>(b =>
{
// Each User can have many UserClaims
b.HasMany(e => e.Claims)
.WithOne()
.HasForeignKey(uc => uc.UserId)
.IsRequired();
});
}
}

Tenga en cuenta que se configura la relación exactamente igual que antes, solo con una propiedad de navegación
especificada en la llamada a HasMany .
Las propiedades de navegación solo existen en el modelo EF, no en la base de datos. Dado que no ha cambiado la
clave externa para la relación, este tipo de cambio de modelo no requiere la base de datos se puede actualizar. Esto
se puede comprobar mediante la adición de una migración después de realizar el cambio. El Up y Down métodos
están vacíos.
Agregar todas las propiedades de navegación del usuario
Con la sección anterior como guía, en el ejemplo siguiente se configura las propiedades de navegación
unidireccional para todas las relaciones de usuario:

public class ApplicationUser : IdentityUser


{
public virtual ICollection<IdentityUserClaim<string>> Claims { get; set; }
public virtual ICollection<IdentityUserLogin<string>> Logins { get; set; }
public virtual ICollection<IdentityUserToken<string>> Tokens { get; set; }
public virtual ICollection<IdentityUserRole<string>> UserRoles { get; set; }
}

public class ApplicationDbContext : IdentityDbContext<ApplicationUser>


{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}

protected override void OnModelCreating(ModelBuilder modelBuilder)


{
base.OnModelCreating(modelBuilder);

modelBuilder.Entity<ApplicationUser>(b =>
{
// Each User can have many UserClaims
b.HasMany(e => e.Claims)
.WithOne()
.HasForeignKey(uc => uc.UserId)
.IsRequired();

// Each User can have many UserLogins


b.HasMany(e => e.Logins)
.WithOne()
.HasForeignKey(ul => ul.UserId)
.IsRequired();

// Each User can have many UserTokens


b.HasMany(e => e.Tokens)
.WithOne()
.HasForeignKey(ut => ut.UserId)
.IsRequired();

// Each User can have many entries in the UserRole join table
b.HasMany(e => e.UserRoles)
.WithOne()
.HasForeignKey(ur => ur.UserId)
.IsRequired();
});
}
}

Agregar las propiedades de navegación del usuario y el rol


Uso de la sección anterior como guía, en el ejemplo siguiente se configura las propiedades de navegación para
todas las relaciones de usuario y el rol:
public class ApplicationUser : IdentityUser
{
public virtual ICollection<IdentityUserClaim<string>> Claims { get; set; }
public virtual ICollection<IdentityUserLogin<string>> Logins { get; set; }
public virtual ICollection<IdentityUserToken<string>> Tokens { get; set; }
public virtual ICollection<ApplicationUserRole> UserRoles { get; set; }
}

public class ApplicationRole : IdentityRole


{
public virtual ICollection<ApplicationUserRole> UserRoles { get; set; }
}

public class ApplicationUserRole : IdentityUserRole<string>


{
public virtual ApplicationUser User { get; set; }
public virtual ApplicationRole Role { get; set; }
}
public class ApplicationDbContext
: IdentityDbContext<
ApplicationUser, ApplicationRole, string,
IdentityUserClaim<string>, ApplicationUserRole, IdentityUserLogin<string>,
IdentityRoleClaim<string>, IdentityUserToken<string>>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}

protected override void OnModelCreating(ModelBuilder modelBuilder)


{
base.OnModelCreating(modelBuilder);

modelBuilder.Entity<ApplicationUser>(b =>
{
// Each User can have many UserClaims
b.HasMany(e => e.Claims)
.WithOne()
.HasForeignKey(uc => uc.UserId)
.IsRequired();

// Each User can have many UserLogins


b.HasMany(e => e.Logins)
.WithOne()
.HasForeignKey(ul => ul.UserId)
.IsRequired();

// Each User can have many UserTokens


b.HasMany(e => e.Tokens)
.WithOne()
.HasForeignKey(ut => ut.UserId)
.IsRequired();

// Each User can have many entries in the UserRole join table
b.HasMany(e => e.UserRoles)
.WithOne(e => e.User)
.HasForeignKey(ur => ur.UserId)
.IsRequired();
});

modelBuilder.Entity<ApplicationRole>(b =>
{
// Each Role can have many entries in the UserRole join table
b.HasMany(e => e.UserRoles)
.WithOne(e => e.Role)
.HasForeignKey(ur => ur.RoleId)
.IsRequired();
});

}
}

Notas:
En este ejemplo también incluye el UserRole unirse a la entidad, que es necesaria para navegar por la relación
de muchos a muchos de los usuarios a Roles.
No olvide cambiar los tipos de las propiedades de navegación para reflejar ese hecho ApplicationXxx tipos
ahora se utilizan en lugar de IdentityXxx tipos.
Recuerde que debe usar el ApplicationXxx en el modelo genérico ApplicationContext definición.
Agregar todas las propiedades de navegación
Uso de la sección anterior como guía, en el ejemplo siguiente se configura las propiedades de navegación para
todas las relaciones en todos los tipos de entidad:

public class ApplicationUser : IdentityUser


{
public virtual ICollection<ApplicationUserClaim> Claims { get; set; }
public virtual ICollection<ApplicationUserLogin> Logins { get; set; }
public virtual ICollection<ApplicationUserToken> Tokens { get; set; }
public virtual ICollection<ApplicationUserRole> UserRoles { get; set; }
}

public class ApplicationRole : IdentityRole


{
public virtual ICollection<ApplicationUserRole> UserRoles { get; set; }
public virtual ICollection<ApplicationRoleClaim> RoleClaims { get; set; }
}

public class ApplicationUserRole : IdentityUserRole<string>


{
public virtual ApplicationUser User { get; set; }
public virtual ApplicationRole Role { get; set; }
}

public class ApplicationUserClaim : IdentityUserClaim<string>


{
public virtual ApplicationUser User { get; set; }
}

public class ApplicationUserLogin : IdentityUserLogin<string>


{
public virtual ApplicationUser User { get; set; }
}

public class ApplicationRoleClaim : IdentityRoleClaim<string>


{
public virtual ApplicationRole Role { get; set; }
}

public class ApplicationUserToken : IdentityUserToken<string>


{
public virtual ApplicationUser User { get; set; }
}
public class ApplicationDbContext
: IdentityDbContext<
ApplicationUser, ApplicationRole, string,
ApplicationUserClaim, ApplicationUserRole, ApplicationUserLogin,
ApplicationRoleClaim, ApplicationUserToken>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}

protected override void OnModelCreating(ModelBuilder modelBuilder)


{
base.OnModelCreating(modelBuilder);

modelBuilder.Entity<ApplicationUser>(b =>
{
// Each User can have many UserClaims
b.HasMany(e => e.Claims)
.WithOne(e => e.User)
.HasForeignKey(uc => uc.UserId)
.IsRequired();

// Each User can have many UserLogins


b.HasMany(e => e.Logins)
.WithOne(e => e.User)
.HasForeignKey(ul => ul.UserId)
.IsRequired();

// Each User can have many UserTokens


b.HasMany(e => e.Tokens)
.WithOne(e => e.User)
.HasForeignKey(ut => ut.UserId)
.IsRequired();

// Each User can have many entries in the UserRole join table
b.HasMany(e => e.UserRoles)
.WithOne(e => e.User)
.HasForeignKey(ur => ur.UserId)
.IsRequired();
});

modelBuilder.Entity<ApplicationRole>(b =>
{
// Each Role can have many entries in the UserRole join table
b.HasMany(e => e.UserRoles)
.WithOne(e => e.Role)
.HasForeignKey(ur => ur.RoleId)
.IsRequired();

// Each Role can have many associated RoleClaims


b.HasMany(e => e.RoleClaims)
.WithOne(e => e.Role)
.HasForeignKey(rc => rc.RoleId)
.IsRequired();
});
}
}

Use las claves compuestas


Las secciones anteriores muestran el cambio del tipo de clave que se usa en el modelo de identidad. Cambiar el
modelo de identidad clave para usar las claves compuestas no es compatible ni recomendable. Usar una clave
compuesta con identidad implica la modificación de cómo interactúa el código del Administrador de identidades
con el modelo. Esta personalización está fuera del ámbito de este documento.
Cambiar los nombres de tabla o columna y las facetas
Para cambiar los nombres de tablas y columnas, llame a base.OnModelCreating . A continuación, agregue la
configuración para reemplazar cualquiera de los valores predeterminados. Por ejemplo, para cambiar el nombre de
todas las tablas de identidad:

protected override void OnModelCreating(ModelBuilder modelBuilder)


{
base.OnModelCreating(modelBuilder);

modelBuilder.Entity<IdentityUser>(b =>
{
b.ToTable("MyUsers");
});

modelBuilder.Entity<IdentityUserClaim<string>>(b =>
{
b.ToTable("MyUserClaims");
});

modelBuilder.Entity<IdentityUserLogin<string>>(b =>
{
b.ToTable("MyUserLogins");
});

modelBuilder.Entity<IdentityUserToken<string>>(b =>
{
b.ToTable("MyUserTokens");
});

modelBuilder.Entity<IdentityRole>(b =>
{
b.ToTable("MyRoles");
});

modelBuilder.Entity<IdentityRoleClaim<string>>(b =>
{
b.ToTable("MyRoleClaims");
});

modelBuilder.Entity<IdentityUserRole<string>>(b =>
{
b.ToTable("MyUserRoles");
});
}

Estos ejemplos utilizan los tipos de identidad predeterminada. Si usa un tipo de aplicación, como ApplicationUser ,
configurar ese tipo en lugar del tipo predeterminado.
El ejemplo siguiente cambia algunos nombres de columna:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);

modelBuilder.Entity<IdentityUser>(b =>
{
b.Property(e => e.Email).HasColumnName("EMail");
});

modelBuilder.Entity<IdentityUserClaim<string>>(b =>
{
b.Property(e => e.ClaimType).HasColumnName("CType");
b.Property(e => e.ClaimValue).HasColumnName("CValue");
});
}

Algunos tipos de columnas de la base de datos pueden configurarse con determinados facetas (por ejemplo, el
máximo string longitud permitida). El ejemplo siguiente establece las longitudes de columna máximo para varios
string propiedades en el modelo:

protected override void OnModelCreating(ModelBuilder modelBuilder)


{
base.OnModelCreating(modelBuilder);

modelBuilder.Entity<IdentityUser>(b =>
{
b.Property(u => u.UserName).HasMaxLength(128);
b.Property(u => u.NormalizedUserName).HasMaxLength(128);
b.Property(u => u.Email).HasMaxLength(128);
b.Property(u => u.NormalizedEmail).HasMaxLength(128);
});

modelBuilder.Entity<IdentityUserToken<string>>(b =>
{
b.Property(t => t.LoginProvider).HasMaxLength(128);
b.Property(t => t.Name).HasMaxLength(128);
});
}

Asignar a un esquema diferente


Los esquemas pueden comportarse de manera diferente a través de proveedores de base de datos. Para SQL
Server, el valor predeterminado es crear todas las tablas de la dbo esquema. Las tablas pueden crearse en un
esquema diferente. Por ejemplo:

protected override void OnModelCreating(ModelBuilder modelBuilder)


{
base.OnModelCreating(modelBuilder);

modelBuilder.HasDefaultSchema("notdbo");
}

Carga diferida
En esta sección, se agrega compatibilidad con servidores proxy de la carga diferida en el modelo de identidad.
Carga diferida es útil porque permite que las propiedades de navegación que se utilizará sin garantizar primero
que están cargado.
Tipos de entidad pueden realizarse adecuados para diferida carga de varias maneras, como se describe en el
documentación de EF Core. Por motivos de simplicidad, use servidores proxy de la carga diferida, lo que requiere:
Instalación de la Microsoft.EntityFrameworkCore.Proxies paquete.
Una llamada a UseLazyLoadingProxies dentro de AddDbContext<TContext>.
Tipos de entidades públicas con public virtual las propiedades de navegación.
El ejemplo siguiente se muestra cómo llamar UseLazyLoadingProxies en Startup.ConfigureServices :

services
.AddDbContext<ApplicationDbContext>(
b => b.UseSqlServer(connectionString)
.UseLazyLoadingProxies())
.AddDefaultIdentity<ApplicationUser>()
.AddEntityFrameworkStores<ApplicationDbContext>();

Consulte los ejemplos anteriores para obtener instrucciones sobre cómo agregar propiedades de navegación a los
tipos de entidad.

Recursos adicionales
Identidad de scaffold en proyectos de ASP.NET Core
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
18/09/2018 • 7 minutes to read • Edit Online

ASP.NET Core Identity utiliza valores predeterminados para la configuración de directiva de contraseñas, bloqueo
y la configuración de la cookie. Esta configuración puede invalidarse en la Startup clase.

Opciones de identidad
El IdentityOptions clase representa las opciones que pueden usarse para configurar el sistema de identidad.
IdentityOptions se debe establecer después llamada AddIdentity o AddDefaultIdentity .

Identidad de notificaciones
IdentityOptions.ClaimsIdentity especifica la ClaimsIdentityOptions con las propiedades mostradas en la tabla
siguiente.

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
El bloqueo se establece el PasswordSignInAsync método:
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
returnUrl = returnUrl ?? Url.Content("~/");

if (ModelState.IsValid)
{
var result = await _signInManager.PasswordSignInAsync(Input.Email,
Input.Password, Input.RememberMe,
lockoutOnFailure: false);
if (result.Succeeded)
{
_logger.LogInformation("User logged in.");
return LocalRedirect(returnUrl);
}
if (result.RequiresTwoFactor)
{
return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl,
Input.RememberMe });
}
if (result.IsLockedOut)
{
_logger.LogWarning("User account locked out.");
return RedirectToPage("./Lockout");
}
else
{
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return Page();
}
}

// If we got this far, something failed, redisplay form


return Page();
}

El código anterior se basa en el Login plantilla de identidad.


Opciones de bloqueo están establecidas StartUp.ConfigureServices :

services.Configure<IdentityOptions>(options =>
{
// Default Lockout settings.
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
options.Lockout.MaxFailedAccessAttempts = 5;
options.Lockout.AllowedForNewUsers = true;
});

El código anterior establece el IdentityOptions LockoutOptions con los valores predeterminados.


Una autenticación correcta restablece el número de intentos de acceso erróneos y restablece el reloj.
IdentityOptions.Lockout especifica la LockoutOptions con las propiedades mostradas en la tabla.

PROPERTY DESCRIPCIÓN DEFAULT

AllowedForNewUsers Determina si un usuario nuevo puede true


estar bloqueado.

DefaultLockoutTimeSpan La cantidad de tiempo un usuario está 5 minutos


bloqueado cuando se produce un
bloqueo.
PROPERTY DESCRIPCIÓN DEFAULT

MaxFailedAccessAttempts El número de intentos de acceso 5


erróneos hasta que un usuario está
bloqueado, si está habilitado el bloqueo.

Contraseña
De forma predeterminada, identidad requiere que las contraseñas contengan un carácter en mayúscula, letra
minúscula, un dígito y un carácter no alfanumérico. Las contraseñas deben tener al menos seis caracteres.
Opciones de contraseña se pueden establecer en Startup.ConfigureServices .

services.Configure<IdentityOptions>(options =>
{
// Default Password settings.
options.Password.RequireDigit = true;
options.Password.RequireLowercase = true;
options.Password.RequireNonAlphanumeric = true;
options.Password.RequireUppercase = true;
options.Password.RequiredLength = 6;
options.Password.RequiredUniqueChars = 1;
});

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

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

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

Requiere el número de caracteres distintos de la contraseña. | 1 |


| RequireLowercase | Requiere un carácter en minúscula en la contraseña. | true | | RequireNonAlphanumeric |
Requiere un carácter que no son alfanuméricos en la contraseña. | true | | RequireUppercase | Requiere un
carácter en mayúsculas en la contraseña. | true |
Inicio de sesión
El código siguiente establece SignIn configuración (para los valores predeterminados):

services.Configure<IdentityOptions>(options =>
{
// Default SignIn settings.
options.SignIn.RequireConfirmedEmail = true;
options.SignIn.RequireConfirmedPhoneNumber = false;
});

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 al iniciar sesión.

RequireConfirmedPhoneNumber Requiere un número de teléfono false


confirmada iniciar sesión.

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 el 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.
PROPERTY DESCRIPCIÓN

PasswordResetTokenProvider Obtiene o establece el IUserTwoFactorTokenProvider usado


para generar tokens que se usan en los correos electrónicos
de restablecimiento de contraseña.

ProviderMap Utilizado para construir un proveedor de tokens de usuario


con la clave que se usa como el nombre del proveedor.

Usuario

services.Configure<IdentityOptions>(options =>
{
// Default User settings.
options.User.AllowedUserNameCharacters =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+";
options.User.RequireUniqueEmail = true;

});

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 única.

Configuración de cookies
Configurar la cookie de la aplicación en Startup.ConfigureServices . ConfigureApplicationCookie debe llamarse
después llamada AddIdentity o AddDefaultIdentity .

services.ConfigureApplicationCookie(options =>
{
options.AccessDeniedPath = "/Identity/Account/AccessDenied";
options.Cookie.Name = "YourAppCookieName";
options.Cookie.HttpOnly = true;
options.ExpireTimeSpan = TimeSpan.FromMinutes(60);
options.LoginPath = "/Identity/Account/Login";
// ReturnUrlParameter requires
//using Microsoft.AspNetCore.Authentication.Cookies;
options.ReturnUrlParameter = CookieAuthenticationDefaults.ReturnUrlParameter;
options.SlidingExpiration = true;
});
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;
});

services.Configure<IdentityOptions>(options =>
{
// Cookie settings
options.Cookies.ApplicationCookie.CookieName = "YourAppCookieName";
options.Cookies.ApplicationCookie.ExpireTimeSpan = TimeSpan.FromDays(150);
options.Cookies.ApplicationCookie.LoginPath = "/Account/LogIn";
options.Cookies.ApplicationCookie.AccessDeniedPath = "/Account/AccessDenied";
options.Cookies.ApplicationCookie.AutomaticAuthenticate = true;
// Requires `using Microsoft.AspNetCore.Authentication.Cookies;`
options.Cookies.ApplicationCookie.AuthenticationScheme =
CookieAuthenticationDefaults.AuthenticationScheme;
options.Cookies.ApplicationCookie.ReturnUrlParameter = CookieAuthenticationDefaults.ReturnUrlParameter;
});

Para obtener más información, consulte CookieAuthenticationOptions.


Configurar la autenticación de Windows en
ASP.NET Core
31/08/2018 • 11 minutes to read • Edit Online

Por Steve Smith y Scott Addie


Se puede configurar la autenticación de Windows para las aplicaciones de ASP.NET Core hospedadas en IIS,
HTTP.sys, o WebListener.

Autenticación de Windows
Autenticación de Windows se basa en el sistema operativo para autenticar usuarios de aplicaciones de
ASP.NET Core. Puede usar la autenticación de Windows cuando el servidor se ejecuta en una red corporativa
mediante identidades de dominio de Active Directory o a otras cuentas de Windows para identificar a los
usuarios. Autenticación de Windows se adapta mejor a los entornos de intranet en el que los usuarios, las
aplicaciones cliente y servidores web pertenecen al mismo dominio de Windows.
Más información acerca de la autenticación de Windows e instalarla para IIS.

Habilitar la autenticación de Windows en una aplicación 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. Seleccione la aplicación Web en la lista de plantillas.
3. Seleccione el Cambiar autenticación y seleccione Windows autenticación.
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 toda la configuración necesaria para
utilizar la autenticación de Windows. En la sección siguiente se muestra cómo configurar manualmente una
aplicación 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, se pueden configurar estas dos propiedades 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 ASP.NET Core para hospedar aplicaciones ASP.NET Core. Autenticación de Windows se
configura 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.
Configuración de IIS
Habilite el servicio de rol IIS para la autenticación de Windows. Para obtener más información, consulte
Habilitar autenticación de Windows en servicios de rol de IIS (consulte el paso 2) .
Middleware de integración de IIS está configurado para autenticar automáticamente las solicitudes de forma
predeterminada. Para obtener más información, consulte Host ASP.NET Core en Windows con IIS: las
opciones de IIS (AutomaticAuthentication).
El módulo ASP.NET Core está configurado para reenviar el token de autenticación de Windows para la
aplicación de forma predeterminada. Para obtener más información, consulte referencia de configuración del
módulo ASP.NET Core: los atributos del elemento aspNetCore.
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 las características de autenticación para el sitio.

Deshabilitar la autenticación anónima y habilitar la autenticación de Windows.

Publique el proyecto en la carpeta del sitio IIS


Con Visual Studio o la CLI de .NET Core, publicar 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


Aunque Kestrel no admite la autenticación de Windows, puede usar HTTP.sys para admitir los escenarios
Auto-hospedados en Windows. El ejemplo siguiente 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();
}

NOTE
HTTP.sys delega en la autenticación de modo kernel con el protocolo de autenticación de Kerberos. La autenticación de
modo usuario no se admite con Kerberos y HTTP.sys. Se debe usar la cuenta de equipo para descifrar el token o el vale
de Kerberos que se obtiene de Active Directory y que el cliente reenvía al servidor para autenticar al usuario. Registre el
nombre de entidad de seguridad de servicio (SPN) para el host, no el usuario de la aplicación.
Habilitar la autenticación de Windows con WebListener
Aunque Kestrel no admite la autenticación de Windows, puede usar WebListener para admitir los escenarios
Auto-hospedados en Windows. El ejemplo siguiente configura el host de la aplicación web para el uso de
WebListener con autenticación de Windows:

public class Program


{
public static void Main(string[] args)
{
var host = new WebHostBuilder()
.UseWebListener(options =>
{
options.ListenerSettings.Authentication.Schemes =
AuthenticationSchemes.Negotiate | AuthenticationSchemes.NTLM;
options.ListenerSettings.Authentication.AllowAnonymous = false;
})
.UseContentRoot(Directory.GetCurrentDirectory())
.UseStartup<Startup>()
.Build();

host.Run();
}
}

NOTE
WebListener delega en la autenticación de modo kernel con el protocolo de autenticación de Kerberos. La autenticación
de modo usuario no se admite con Kerberos y WebListener. Se debe usar la cuenta de equipo para descifrar el token o el
vale de Kerberos que se obtiene de Active Directory y que el cliente reenvía al servidor para autenticar al usuario.
Registre el nombre de entidad de seguridad de servicio (SPN) para el host, no el usuario de la aplicación.

Trabajar con la autenticación de Windows


Estado de configuración del acceso anónimo determina la manera en que el [Authorize] y [AllowAnonymous]
atributos se utilizan en la aplicación. Las dos secciones siguientes explican cómo controlar los Estados de
configuración de permitidos y no permitidos del acceso anónimo.
No permitir el acceso anónimo
Cuando está habilitada 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 no permitir el acceso anónimo, la solicitud nunca llega a la aplicación. Por este motivo, la
[AllowAnonymous] atributo no es aplicable.

Permitir el acceso anónimo


Cuando se habilitan tanto la autenticación de Windows como el acceso anónimo, utilice el [Authorize] y
[AllowAnonymous] atributos. El [Authorize] atributo permite proteger partes de la aplicación que realmente
requiera autenticación de Windows. El [AllowAnonymous] invalidaciones de atributo [Authorize] atributo uso
dentro de las aplicaciones que permite el acceso anónimo. Consulte autorización sencilla para obtener detalles
de uso del atributo.
En ASP.NET Core 2.x, el [Authorize] atributo requiere configuración adicional en Startup.cs Desafíe a las
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, se presentan a los usuarios que no tienen autorización para acceder a una página con una
respuesta HTTP 403 vacía. El StatusCodePages middleware puede configurarse 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 usa 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, utilizando la identidad de proceso o grupo de servidores de aplicación. Si necesita realizar
explícitamente una acción en nombre de un usuario, 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 puede utilizarse
para escenarios complejos. Por ejemplo, el ajuste de las solicitudes de todas o las cadenas de middleware no es
compatible ni recomendable.
Proveedores de almacenamiento personalizados para
ASP.NET Core Identity
18/09/2018 • 20 minutes to read • Edit Online

Por Steve Smith


ASP.NET Core Identity es un sistema extensible que le permite crear un proveedor de almacenamiento
personalizado y conectarlo a la aplicación. Este tema describe cómo crear un proveedor de almacenamiento
personalizados para ASP.NET Core Identity. Se tratan 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 ASP.NET Core Identity almacena información de usuario en una base de
datos de SQL Server mediante Entity Framework Core. Para muchas aplicaciones, este enfoque funciona bien. Sin
embargo, es preferible utilizar un mecanismo de persistencia diferente o un esquema de datos. Por ejemplo:
Usa Azure Table Storage u otro almacén de datos.
Las tablas de base de datos tienen una estructura diferente.
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 en la aplicación.
ASP.NET Core Identity se incluye en las plantillas de proyecto en Visual Studio con la opción "Cuentas de usuario
individuales".
Si utiliza la CLI de .NET Core, 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 que se llama a los administradores y los almacenes. Los
administradores son clases de alto nivel que un desarrollador de aplicaciones que se usa para realizar operaciones
como la creación de un usuario de identidad. Almacenes son clases de nivel inferior que especifican cómo se
conservan las entidades, como usuarios y roles. Almacenes de seguir la patrón de repositorio y se acopla
estrechamente con el mecanismo de persistencia. Los administradores se desacoplan de los almacenes, lo que
significa que puede reemplazar el mecanismo de persistencia sin cambiar el código de aplicación (excepto para la
configuración).
El diagrama siguiente muestra cómo una aplicación web se interactúa con los administradores, mientras que los
almacenes de 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 esta capa de acceso a datos (los cuadros verdes y gris en el diagrama
anterior). No es necesario personalizar los administradores o el código de aplicación que interactúa con ellos (los
cuadros azules anteriores).
Al crear una nueva instancia de UserManager o RoleManager proporcionar el tipo de la clase de usuario y pasar una
instancia de la clase de almacenamiento como un argumento. Este enfoque le permite conectar sus clases
personalizadas en ASP.NET Core.
Volver a configurar la aplicación para usar el nuevo proveedor de almacenamiento se muestra cómo crear una
instancia de UserManager y RoleManager con un almacén personalizado.

ASP.NET Core Identity almacena tipos de datos


ASP.NET Core Identity tipos de datos que 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 necesita 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) sobre el usuario que representan la identidad del usuario. Puede
permitir que una 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 externos (como Facebook o una cuenta de Microsoft) que se
usará al iniciar sesión en un usuario. Ejemplo
Roles
Grupos de autorización para su 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 sobre cómo crear los repositorios o
las clases de acceso a datos; Cuando se trabaja con ASP.NET Core Identity proporciona algunas sugerencias acerca
de las decisiones de diseño.
Tiene mucha libertad al diseñar la capa de acceso a datos para un proveedor de almacén personalizado. Solo
necesita crear mecanismos de persistencia para las características que piensa usar en su aplicación. Por ejemplo, si
no usa roles en su aplicación, no es necesario crear 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 que funcione con la estructura de su implementación de almacenamiento.
La capa de acceso a datos proporciona la lógica para guardar los datos de ASP.NET Core Identity a un origen de
datos. La capa de acceso a datos para su proveedor de almacenamiento personalizado puede 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, que normalmente se suministran mediante la inserción de
dependencias. Ejemplo.
Almacenamiento de información de usuario
Almacena y recupera información de usuario (por ejemplo, hash de nombre y la contraseña de usuario). Ejemplo
Almacenamiento de rol
Almacena y recupera información de funciones (por ejemplo, el nombre del 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 sólo 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 clase
IdentityUser.
Como mínimo, la clase de usuario debe incluir un Id y un UserName propiedad.
El IdentityUser clase define las propiedades que el UserManager llamadas al realizar las operaciones solicitadas. El
tipo predeterminado de la Id propiedad es una cadena, pero 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 tipos de datos.

Personalizar el almacén de 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<TUser> clase. En su UserStore clase, implemente IUserStore<TUser> y las
interfaces opcionales requeridas. Seleccione qué interfaces opcionales para la implementación en función de la
funcionalidad ofrecida en la aplicación.
Interfaces opcionales
IUserRoleStore
IUserClaimStore
IUserPasswordStore
IUserSecurityStampStore
IUserEmailStore
IPhoneNumberStore
IQueryableUserStore
IUserLoginStore
IUserTwoFactorStore
IUserLockoutStore
Las interfaces opcionales que se heredan de IUserStore<TUser> . Puede ver un usuario de ejemplo implementada
parcialmente almacenar en el aplicación de ejemplo.
Dentro de la UserStore (clase), utilice las clases de acceso de datos que ha creado para llevar a cabo las
operaciones. Estos se pasan mediante la inserción de dependencia. Por ejemplo, en el servidor SQL Server con la
implementación de Dapper, el UserStore clase tiene el CreateAsync método que utiliza 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> es la única interfaz 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 las notificaciones de
usuario. Contiene métodos para agregar, quitar y recuperar las notificaciones de usuario.
IUserLoginStore
El IUserLoginStore<TUser> define los métodos que se implementan para habilitar los proveedores de
autenticación externos. Contiene métodos para agregar, quitar y recuperar los inicios de sesión de usuario y un
método para recuperar un usuario basado en la información de inicio de sesión.
IUserRoleStore
El IUserRoleStore<TUser> interfaz define los métodos que implementa para asignar un usuario a un rol.
Contiene métodos para agregar, quitar y recuperar roles de un 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 implementa para conservar las contraseñas
con hash. Contiene métodos para obtener y establecer la contraseña con algoritmo 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 agrega o quita los 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 implemente 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 los 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 implementa para almacenar información acerca
del bloqueo de una cuenta. Contiene métodos para realizar el seguimiento de intentos de acceso incorrectos 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 su 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, es posible que
desee crear sus propias versiones de estas clases y definir las propiedades de la aplicación. Sin embargo, a veces es
más eficaz para no cargar estas entidades en memoria al realizar operaciones básicas (como agregar o quitar
notificaciones de un usuario). En su lugar, las clases de almacén de back-end pueden ejecutar estas operaciones
directamente en el origen de datos. Por ejemplo, el UserStore.GetClaimsAsync puede llamar al método el
userClaimTable.FindByUserId(user.Id) método para ejecutar una consulta en que directamente de la tabla y
devuelve una lista de notificaciones.

Personalizar la clase de función


Al implementar un proveedor de almacenamiento de información de roles, puede crear un tipo de rol
personalizado. No tiene que implementar una interfaz determinada, pero debe tener un Id y normalmente
tendrán un Name propiedad.
La siguiente es una clase de función 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 roles. Esta
clase es equivalente a la RoleStore<; TRole> clase. En el RoleStore (clase), implementa la IRoleStore<TRole> y,
opcionalmente, el IQueryableRoleStore<TRole> interfaz.
IRoleStore<; TRole>
El IRoleStore<; TRole> interfaz define los métodos para implementar en la clase de almacén de rol. Contiene
métodos para crear, actualizar, eliminar y recuperar roles.
RoleStore<; TRole>
Para personalizar RoleStore , cree una clase que implementa el IRoleStore<TRole> interfaz.

Volver a configurar la aplicación para que use un nuevo proveedor de


almacenamiento
Una vez que ha implementado un proveedor de almacenamiento, configure la aplicación para que lo utilicen. 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 se encuentra 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 de su proveedor de almacenamiento.
4. En el ConfigureServices método, cambio el AddIdentity método para usar sus tipos personalizados. Puede
crear sus propios métodos de extensión para este propósito. Consulte IdentityServiceCollectionExtensions para
obtener un ejemplo.
5. Si está utilizando Roles, actualice el RoleManager para usar su RoleStore clase.
6. Actualice 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 personalizados para la identidad ASP.NET 4.x
ASP.NET Core Identity -este repositorio incluye vínculos a la Comunidad que mantiene los proveedores de
almacenes.
Autenticación con Facebook, Google y proveedores
externos en ASP.NET Core
05/09/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 los tokens Id. de aplicación y Secreto de la
aplicación durante el proceso de registro. La nomenclatura puede variar en función del proveedor. Estos
tokens representan las credenciales que usa la aplicación para acceder a su API. Los tokens constituyen los
"secretos" que se pueden vincular a la configuración de la aplicación con la ayuda de Secret Manager. Secret
Manager es una alternativa más segura al almacenamiento de los tokens en un archivo de configuración,
como, por ejemplo, appsettings.json.

IMPORTANT
Secret Manager solo está pensado para fines de desarrollo. Puede almacenar y proteger sus secretos de producción y
pruebas de Azure con el proveedor de configuración de Azure Key Vault.

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 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
Cuando la aplicación requiera varios proveedores, encadene los métodos de extensión del proveedor detrás de
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.
Le recomendamos que conserve los datos adicionales sobre el usuario y sus tokens de acceso y
actualización. Para obtener más información, vea Conservar notificaciones adicionales y los tokens de
proveedores externos en ASP.NET Core.
Configuración de inicio de sesión externo de
Facebook en ASP.NET Core
23/08/2018 • 8 minutes to read • Edit Online

Por Valeriy Novytskyy y Rick Anderson


Este tutorial muestra cómo permitir que los usuarios iniciar sesión con su cuenta de Facebook con un proyecto de
ASP.NET Core 2.0 de ejemplo creado en el página anterior. La autenticación de Facebook requiere la
Microsoft.AspNetCore.Authentication.Facebook paquete NuGet. Empezamos creando un App identificador
Facebook siguiendo el pasos oficiales.

Crear la aplicación en Facebook


Navegue hasta la app de desarrolladores de Facebook página e inicie sesión. Si no dispone de una cuenta
de Facebook, use el suscribirse a Facebook vínculo en la página de inicio de sesión para crear uno.
Pulse el 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 Quickstart se iniciará con elija 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 anexado a la URI de redirección de OAuth válido 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 configuración > básica vínculo en el panel de navegación izquierdo.
En esta página, tome nota de su App ID y su App Secret . En la siguiente sección, va a agregar tanto en la
aplicación de ASP.NET Core:
Al implementar el sitio tiene que volver a visitar la inicio de sesión de Facebook página de la instalación
y registrar un nuevo URI público.

Store Id. de aplicación de Facebook y secreto de la aplicación


Vincular configuración confidencial, como Facebook App ID y App Secret para la configuración de aplicación
mediante el Secret Manager. Para los fines de este tutorial, asigne el 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 Secret Manager:

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 distintos fines. 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. Existen sobrecargas que permiten configurar las propiedades del esquema, el
nombre de esquema y nombre para mostrar.
Cuando la aplicación requiera varios proveedores, encadene los métodos de extensión del proveedor detrás de
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. Las 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 inicie 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:

Dirección pública de perfil y correo electrónico de solicitudes de autenticación de Facebook de forma


predeterminada:
Una vez que escriba sus credenciales de Facebook se redirigen a su sitio donde puede establecer su correo
electrónico.
Ha iniciado sesión con sus credenciales de Facebook:

Solución de problemas
ASP.NET Core 2.x solo: identidad si no está configurado mediante una llamada a services.AddIdentity en
ConfigureServices , intentando autenticarse producirá ArgumentException: se debe proporcionar la opción
'SignInScheme'. La plantilla de proyecto que se usa en este tutorial, se garantiza que esto se realiza.
Si la base de datos de sitio no se ha creado aplicando a la migración inicial, obtendrá error en una operación
de base de datos al procesar la solicitud error. Pulse aplicar migraciones para crear la base de datos y
actualizar para continuar más allá del error.

Pasos siguientes
Este artículo, mostramos 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 publique su sitio web a la aplicación web de Azure, debe restablecer el 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 Azure portal. 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 distintos fines. 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. Existen sobrecargas que permiten configurar las propiedades del esquema, el
nombre de esquema y nombre para mostrar.
Cuando la aplicación requiera varios proveedores, encadene los métodos de extensión del proveedor detrás de
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 distintos fines. 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. Existen sobrecargas que permiten configurar las propiedades del esquema,
el nombre de esquema y nombre para mostrar.
Cuando la aplicación requiera varios proveedores, encadene los métodos de extensión del proveedor detrás de
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 distintos fines. 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. Existen sobrecargas que permiten configurar las propiedades del esquema, el
nombre de esquema y nombre para mostrar.
Cuando la aplicación requiera varios proveedores, encadene los métodos de extensión del proveedor detrás de
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


Cuando la aplicación requiera varios proveedores, encadene los métodos de extensión del proveedor detrás de
AddAuthentication:

services.AddAuthentication()
.AddMicrosoftAccount(microsoftOptions => { ... })
.AddGoogle(googleOptions => { ... })
.AddTwitter(twitterOptions => { ... })
.AddFacebook(facebookOptions => { ... });
Conservar notificaciones adicionales y los tokens de
proveedores externos en ASP.NET Core
31/08/2018 • 10 minutes to read • Edit Online

Por Luke Latham


Una aplicación ASP.NET Core puede establecer notificaciones adicionales y los tokens de proveedores de
autenticación externos, como Facebook, Google, Microsoft y Twitter. Cada proveedor revela información distinta
acerca de los usuarios en su plataforma, pero el patrón para recibir y transformar los datos de usuario en
notificaciones adicionales es el mismo.
Vea o descargue el código de ejemplo (cómo descargarlo)

Requisito previo
Decida qué proveedores de autenticación externos para admitir en la aplicación. Para cada proveedor, registrar la
aplicación y obtener un identificador de cliente y secreto de cliente. Para obtener más información, consulta
Autenticación con Facebook, Google y proveedores externos en ASP.NET Core. El aplicación de ejemplo usa el
proveedor de autenticación de Google.

Configuración del proveedor de autenticación


Establece el identificador de cliente y el secreto de cliente
El proveedor de autenticación OAuth establece una relación de confianza con una aplicación mediante un
identificador de cliente y secreto de cliente. Id. de cliente y los valores de secreto de cliente se crean para la
aplicación mediante el proveedor de autenticación externo cuando la aplicación esté registrada con el proveedor.
Cada proveedor externo que usa la aplicación debe configurarse de forma independiente con Id. de cliente y el
secreto de cliente del proveedor. Para obtener más información, vea los temas de proveedor de autenticación
externo que se aplican a su escenario:
Autenticación con Facebook
Autenticación con Google
Autenticación con Microsoft
Autenticación con Twitter
Otros proveedores de autenticación
OpenIdConnect
La aplicación de ejemplo configura el proveedor de autenticación de Google con un identificador de cliente y secreto
de cliente proporcionado por Google:
services.AddAuthentication().AddGoogle(options =>
{
// Provide the Google Client ID
options.ClientId = "XXXXXXXXXXX.apps.googleusercontent.com";
// Provide the Google Secret
options.ClientSecret = "g4GZ2#...GD5Gg1x";
options.Scope.Add("https://www.googleapis.com/auth/plus.login");
options.ClaimActions.MapJsonKey(ClaimTypes.Gender, "gender");
options.SaveTokens = true;
options.Events.OnCreatingTicket = ctx =>
{
List<AuthenticationToken> tokens = ctx.Properties.GetTokens()
as List<AuthenticationToken>;
tokens.Add(new AuthenticationToken()
{
Name = "TicketCreated",
Value = DateTime.UtcNow.ToString()
});
ctx.Properties.StoreTokens(tokens);
return Task.CompletedTask;
};
});

Establecer el ámbito de autenticación


Especifique la lista de permisos para recuperar el proveedor especificando el Scope. Ámbitos de autenticación para
los proveedores externos comunes aparecen en la tabla siguiente.

PROVEEDOR ÁMBITO

Facebook https://www.facebook.com/dialog/oauth

Google https://www.googleapis.com/auth/plus.login

Microsoft https://login.microsoftonline.com/common/oauth2/v2.0/authorize

Twitter https://api.twitter.com/oauth/authenticate

La aplicación de ejemplo agrega Google plus.login ámbito de sesión de Google + en los permisos de solicitud:

services.AddAuthentication().AddGoogle(options =>
{
// Provide the Google Client ID
options.ClientId = "XXXXXXXXXXX.apps.googleusercontent.com";
// Provide the Google Secret
options.ClientSecret = "g4GZ2#...GD5Gg1x";
options.Scope.Add("https://www.googleapis.com/auth/plus.login");
options.ClaimActions.MapJsonKey(ClaimTypes.Gender, "gender");
options.SaveTokens = true;
options.Events.OnCreatingTicket = ctx =>
{
List<AuthenticationToken> tokens = ctx.Properties.GetTokens()
as List<AuthenticationToken>;
tokens.Add(new AuthenticationToken()
{
Name = "TicketCreated",
Value = DateTime.UtcNow.ToString()
});
ctx.Properties.StoreTokens(tokens);
return Task.CompletedTask;
};
});
Asignar claves de datos de usuario y crear notificaciones
En las opciones del proveedor, especifique un MapJsonKey para cada clave de datos de usuario del proveedor
externo JSON para la identidad de aplicación leer en Inicio de sesión. Para obtener más información sobre los tipos
de notificación, consulte ClaimTypes.
La aplicación de ejemplo crea un Gender notificación desde el gender clave en los datos de usuario de Google:

services.AddAuthentication().AddGoogle(options =>
{
// Provide the Google Client ID
options.ClientId = "XXXXXXXXXXX.apps.googleusercontent.com";
// Provide the Google Secret
options.ClientSecret = "g4GZ2#...GD5Gg1x";
options.Scope.Add("https://www.googleapis.com/auth/plus.login");
options.ClaimActions.MapJsonKey(ClaimTypes.Gender, "gender");
options.SaveTokens = true;
options.Events.OnCreatingTicket = ctx =>
{
List<AuthenticationToken> tokens = ctx.Properties.GetTokens()
as List<AuthenticationToken>;
tokens.Add(new AuthenticationToken()
{
Name = "TicketCreated",
Value = DateTime.UtcNow.ToString()
});
ctx.Properties.StoreTokens(tokens);
return Task.CompletedTask;
};
});

En OnPostConfirmationAsync, un IdentityUser ( ApplicationUser ) ha iniciado sesión en la aplicación con


SignInAsync. Durante el inicio de sesión en proceso, el UserManager<TUser> puede almacenar un
ApplicationUser de notificación para los datos de usuario disponibles en el Principal.

En la aplicación de ejemplo, OnPostConfirmationAsync (Account/ExternalLogin.cshtml.cs) establece un Gender de


notificación para firmado en ApplicationUser :
public async Task<IActionResult> OnPostConfirmationAsync(
string returnUrl = null)
{
if (ModelState.IsValid)
{
// Get the information about the user from the external login
// provider
var info = await _signInManager.GetExternalLoginInfoAsync();

if (info == null)
{
throw new ApplicationException(
"Error loading external login data during confirmation.");
}

var user = new ApplicationUser


{
UserName = Input.Email,
Email = Input.Email
};
var result = await _userManager.CreateAsync(user);

if (result.Succeeded)
{
result = await _userManager.AddLoginAsync(user, info);

if (result.Succeeded)
{
// Copy over the gender claim
await _userManager.AddClaimAsync(user,
info.Principal.FindFirst(ClaimTypes.Gender));

// Include the access token in the properties


var props = new AuthenticationProperties();
props.StoreTokens(info.AuthenticationTokens);

await _signInManager.SignInAsync(user, props,


authenticationMethod: info.LoginProvider);
_logger.LogInformation(
"User created an account using {Name} provider.",
info.LoginProvider);

return LocalRedirect(Url.GetLocalUrl(returnUrl));
}
}

foreach (var error in result.Errors)


{
ModelState.AddModelError(string.Empty, error.Description);
}
}

ReturnUrl = returnUrl;

return Page();
}

Guarde el token de acceso


SaveTokens define si se deben almacenar los tokens de acceso y actualización en el AuthenticationProperties
después de una autorización correcta. SaveTokens se establece en false de forma predeterminada para reducir el
tamaño de la cookie de autenticación final.
La aplicación de ejemplo establece el valor de SaveTokens a true en GoogleOptions:
services.AddAuthentication().AddGoogle(options =>
{
// Provide the Google Client ID
options.ClientId = "XXXXXXXXXXX.apps.googleusercontent.com";
// Provide the Google Secret
options.ClientSecret = "g4GZ2#...GD5Gg1x";
options.Scope.Add("https://www.googleapis.com/auth/plus.login");
options.ClaimActions.MapJsonKey(ClaimTypes.Gender, "gender");
options.SaveTokens = true;
options.Events.OnCreatingTicket = ctx =>
{
List<AuthenticationToken> tokens = ctx.Properties.GetTokens()
as List<AuthenticationToken>;
tokens.Add(new AuthenticationToken()
{
Name = "TicketCreated",
Value = DateTime.UtcNow.ToString()
});
ctx.Properties.StoreTokens(tokens);
return Task.CompletedTask;
};
});

Cuando OnPostConfirmationAsync se ejecuta, almacenar el token de acceso (ExternalLoginInfo.AuthenticationTokens)


del proveedor externo en el ApplicationUser del AuthenticationProperties .
La aplicación de ejemplo guarda en el token de acceso:
OnPostConfirmationAsync – Ejecuta de nuevo registro de usuario.
OnGetCallbackAsync – Se ejecuta cuando inicia sesión un usuario registrado anteriormente en la aplicación.

Account/ExternalLogin.cshtml.cs:
public async Task<IActionResult> OnPostConfirmationAsync(
string returnUrl = null)
{
if (ModelState.IsValid)
{
// Get the information about the user from the external login
// provider
var info = await _signInManager.GetExternalLoginInfoAsync();

if (info == null)
{
throw new ApplicationException(
"Error loading external login data during confirmation.");
}

var user = new ApplicationUser


{
UserName = Input.Email,
Email = Input.Email
};
var result = await _userManager.CreateAsync(user);

if (result.Succeeded)
{
result = await _userManager.AddLoginAsync(user, info);

if (result.Succeeded)
{
// Copy over the gender claim
await _userManager.AddClaimAsync(user,
info.Principal.FindFirst(ClaimTypes.Gender));

// Include the access token in the properties


var props = new AuthenticationProperties();
props.StoreTokens(info.AuthenticationTokens);

await _signInManager.SignInAsync(user, props,


authenticationMethod: info.LoginProvider);
_logger.LogInformation(
"User created an account using {Name} provider.",
info.LoginProvider);

return LocalRedirect(Url.GetLocalUrl(returnUrl));
}
}

foreach (var error in result.Errors)


{
ModelState.AddModelError(string.Empty, error.Description);
}
}

ReturnUrl = returnUrl;

return Page();
}
public async Task<IActionResult> OnGetCallbackAsync(
string returnUrl = null, string remoteError = null)
{
if (remoteError != null)
{
ErrorMessage = $"Error from external provider: {remoteError}";

return RedirectToPage("./Login");
}

var info = await _signInManager.GetExternalLoginInfoAsync();

if (info == null)
{
return RedirectToPage("./Login");
}

// Sign in the user with this external login provider if the user
// already has a login
var result = await _signInManager.ExternalLoginSignInAsync(
info.LoginProvider, info.ProviderKey, isPersistent: false,
bypassTwoFactor : true);

if (result.Succeeded)
{
// Store the access token and resign in so the token is included in
// in the cookie
var user = await _userManager.FindByLoginAsync(info.LoginProvider,
info.ProviderKey);

var props = new AuthenticationProperties();


props.StoreTokens(info.AuthenticationTokens);

await _signInManager.SignInAsync(user, props, info.LoginProvider);

_logger.LogInformation(
"{Name} logged in with {LoginProvider} provider.",
info.Principal.Identity.Name, info.LoginProvider);

return LocalRedirect(Url.GetLocalUrl(returnUrl));
}

if (result.IsLockedOut)
{
return RedirectToPage("./Lockout");
}
else
{
// If the user does not have an account, then ask the user to
// create an account
ReturnUrl = returnUrl;
LoginProvider = info.LoginProvider;

if (info.Principal.HasClaim(c => c.Type == ClaimTypes.Email))


{
Input = new InputModel
{
Email = info.Principal.FindFirstValue(ClaimTypes.Email)
};
}

return Page();
}
}

Cómo agregar tokens personalizados adicionales


Para demostrar cómo agregar un token personalizado, que se almacena como parte de SaveTokens , la aplicación de
ejemplo agrega un AuthenticationToken con el actual DateTime para un AuthenticationToken.Name de
TicketCreated :

services.AddAuthentication().AddGoogle(options =>
{
// Provide the Google Client ID
options.ClientId = "XXXXXXXXXXX.apps.googleusercontent.com";
// Provide the Google Secret
options.ClientSecret = "g4GZ2#...GD5Gg1x";
options.Scope.Add("https://www.googleapis.com/auth/plus.login");
options.ClaimActions.MapJsonKey(ClaimTypes.Gender, "gender");
options.SaveTokens = true;
options.Events.OnCreatingTicket = ctx =>
{
List<AuthenticationToken> tokens = ctx.Properties.GetTokens()
as List<AuthenticationToken>;
tokens.Add(new AuthenticationToken()
{
Name = "TicketCreated",
Value = DateTime.UtcNow.ToString()
});
ctx.Properties.StoreTokens(tokens);
return Task.CompletedTask;
};
});

Instrucciones de la aplicación de ejemplo


La aplicación de ejemplo muestra cómo:
Obtenga el sexo del usuario de Google y almacena una notificación de género con el valor.
Store el token de acceso de Google en el usuario AuthenticationProperties .

Para usar la aplicación de ejemplo:


1. Registrar la aplicación y obtener un identificador de cliente válido y el secreto de cliente para la autenticación de
Google. Para obtener más información, consulta Programa de instalación de inicio de sesión externo de Google
en ASP.NET Core.
2. Proporcione el identificador de cliente y secreto de cliente a la aplicación en el GoogleOptions de
Startup.ConfigureServices .
3. Ejecute la aplicación y solicite la página Mis notificaciones. Cuando el usuario no se ha iniciado sesión, la
aplicación se redirige a Google. Inicie sesión con Google. Google redirige al usuario a la aplicación (
/Home/MyClaims ). El usuario se autentica y se carga la página Mis notificaciones. Está presente en la notificación
de género notificaciones de usuario con el valor obtenido de Google. El token de acceso aparece en el las
propiedades de autenticación.
User Claims

http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier
b36a7b09-9135-4810-b7a5-78697ff23e99
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name
username@gmail.com
AspNet.Identity.SecurityStamp
29G2TB881ATCUQFJSRFG1S0QJ0OOAWVT
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/gender
female
http://schemas.microsoft.com/ws/2008/06/identity/claims/authenticationmethod
Google

Authentication Properties

.Token.access_token
bv42.Dgw...GQMv9ArLPs
.Token.token_type
Bearer
.Token.expires_at
2018-08-27T19:08:00.0000000+00:00
.Token.TicketCreated
8/27/2018 6:08:00 PM
.TokenNames
access_token;token_type;expires_at;TicketCreated
.issued
Mon, 27 Aug 2018 18:08:05 GMT
.expires
Mon, 10 Sep 2018 18:08:05 GMT
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 distintos fines. 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. Existen sobrecargas que permiten configurar las propiedades del esquema, el
nombre de esquema y nombre para mostrar.
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();
// …
}
09/08/2018 • 15 minutes to read • Edit Online

Consulte este archivo PDF para el núcleo de ASP.NET 1.1 y versión 2.1.

Confirmación de la cuenta y la recuperación de contraseñas


en ASP.NET Core
Por Rick Anderson y Joe Audette
Este tutorial muestra cómo crear 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
Entity Framework Core

Requisitos previos
SDK de .NET Core 2.1 o versiones posteriores

Crear una aplicación web y aplicar la técnica scaffolding identidad


Visual Studio
CLI de .NET Core
En Visual Studio, cree un nuevo aplicación Web proyecto denominado WebPWrecover.
Seleccione ASP.NET Core 2.1.
Mantenga el valor predeterminado autenticación establecido en sin autenticación. La autenticación se
agrega en el paso siguiente.
En el paso siguiente:
Establece la página de diseño en ~/Pages/Shared/_Layout.cshtml
Seleccione cuenta/Register
Cree un nuevo clase de contexto de datos
Siga las instrucciones de Habilitar autenticación:
Agregar app.UseAuthentication(); a Startup.Configure
Agregar <partial name="_LoginPartial" /> para el archivo de diseño.

Nuevo registro de usuario de prueba


Ejecute la aplicación, seleccione la registrar vincular y registrar un usuario. En este momento, 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, se actualiza el código para que los nuevos usuarios no pueden iniciar
sesión hasta que se valida su correo electrónico.
Vista de la base de datos de identidad
Visual Studio
CLI de .NET Core
Desde el vista menú, seleccione Explorador de objetos de SQL Server (SSOX).
Vaya a MSSQLLocalDB (13 de SQL Server) (localdb). Haga doble clic en dbo. AspNetUsers > ver
datos:

Tenga en cuenta la tabla EmailConfirmed campo es False .


Es posible que desee 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 resulta más fácil en los pasos siguientes.

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 persona (es decir, se hayan
registrado con el correo electrónico de otra persona). Supongamos 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 el correo no deseado de la aplicación. Supongamos que el usuario
registrado accidentalmente como "ylo@example.com" y no se vio el error ortográfico de "yli". No podrán usar la
recuperación de contraseña porque la aplicación no tiene su correo electrónico correcto. Confirmación por
correo electrónico proporciona una protección limitada de bots. Confirmación por correo electrónico no
proporciona protección contra usuarios malintencionados con varias cuentas de correo electrónico.
Por lo general desea impedir que todos los datos del sitio web de registro antes de que tengan un correo
electrónico confirmado nuevos usuarios.
Actualización Areas/Identity/IdentityHostingStartup.cs para requerir un correo electrónico de confirmación:
public class IdentityHostingStartup : IHostingStartup
{
public void Configure(IWebHostBuilder builder)
{
builder.ConfigureServices((context, services) => {
services.AddDbContext<WebPWrecoverContext>(options =>
options.UseSqlServer(
context.Configuration.GetConnectionString("WebPWrecoverContextConnection")));

services.AddDefaultIdentity<IdentityUser>(config =>
{
config.SignIn.RequireConfirmedEmail = true;
})
.AddEntityFrameworkStores<WebPWrecoverContext>();
});
}
}

config.SignIn.RequireConfirmedEmail = true; impide que los usuarios registrados iniciar sesión hasta que se
confirme su correo electrónico.
Configurar el proveedor de correo electrónico
En este tutorial, SendGrid se usa para enviar correo electrónico. Necesita una cuenta de SendGrid y una clave
para enviar correo electrónico. Puede usar otros proveedores de correo electrónico. ASP.NET Core 2.x incluye
System.Net.Mail , lo que 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.
Cree una clase para recuperar la clave de protección del correo electrónico. Para este ejemplo, crear
Services/AuthMessageSenderOptions.cs:

public class AuthMessageSenderOptions


{
public string SendGridUser { get; set; }
public string SendGridKey { get; set; }
}

Configurar secretos de usuario de SendGrid


Agregar un único <UserSecretsId> valor para el <PropertyGroup> elemento del archivo del proyecto:

<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<UserSecretsId>aspnet-WebPWrecover-1234</UserSecretsId>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.4" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.1.1" />
<PackageReference Include="SendGrid" Version="9.9.0" />
</ItemGroup>

</Project>

Establecer el SendGridUser y SendGridKey con el herramienta secret manager. Por ejemplo:


C:/WebAppl>dotnet user-secrets set SendGridUser RickAndMSFT
info: Successfully saved SendGridUser = RickAndMSFT to the secret store.

En Windows, Secret Manager almacena los pares de claves/valor en un secrets.json de archivos en el


%APPDATA%/Microsoft/UserSecrets/<WebAppName-userSecretsId> directory.

El contenido de la secrets.json archivo no se cifran. El secrets.json archivo se muestra a continuación (el


SendGridKey se ha quitado el valor.)

{
"SendGridUser": "RickAndMSFT",
"SendGridKey": "<key removed>"
}

Para obtener más información, consulte el patrón de opciones y configuración.


Instalar SendGrid
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:
Visual Studio
CLI de .NET Core
Desde la consola de administrador de paquetes, escriba el siguiente comando:

Install-Package SendGrid

Consulte comience gratis con SendGrid para registrar una cuenta gratuita de SendGrid.
Implementar IEmailSender
Para implementar IEmailSender , crear Services/EmailSender.cs con código similar al siguiente:
using Microsoft.AspNetCore.Identity.UI.Services;
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));

// Disable click tracking.


// See https://sendgrid.com/docs/User_Guide/Settings/tracking.html
msg.TrackingSettings = new TrackingSettings
{
ClickTracking = new ClickTracking { Enable = false }
};

return client.SendEmailAsync(msg);
}
}
}

Configurar el inicio para admitir correo electrónico


Agregue el código siguiente a la ConfigureServices método en el Startup.cs archivo:
Agregar EmailSender como un servicio de singleton.
Registrar el AuthMessageSenderOptions instancia de configuración.
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});

services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

// requires
// using Microsoft.AspNetCore.Identity.UI.Services;
// using WebPWrecover.Services;
services.AddSingleton<IEmailSender, EmailSender>();
services.Configure<AuthMessageSenderOptions>(Configuration);
}

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 Areas/Identity/Pages/Account/Register.cshtml.cs.

Impedir que los usuarios recién registrados que se ha iniciado sesión automáticamente al marcar 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 ?? Url.Content("~/");
if (ModelState.IsValid)
{
var user = new IdentityUser { 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.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();
}

Registrar, confirmar correo electrónico y restablecimiento de


contraseña
Ejecutar la aplicación web y probar el flujo de recuperación de contraseña y confirmación de la cuenta.
Ejecute la aplicación y registrar un nuevo usuario
Compruebe su correo electrónico para el vínculo de confirmación de la cuenta. Consulte depurar correo
electrónico si no recibe el correo electrónico.
Haga clic en el vínculo para confirmar su 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:

Es posible que deba expandir la barra de navegación para ver el nombre de usuario.
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 la prueba
Si ha iniciado sesión, seleccione Logout.
Seleccione el iniciarla vínculo y seleccione el ¿olvidó su contraseña? vínculo.
Escriba el correo electrónico usado 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 se ha restablecido
correctamente su contraseña, puede iniciar sesión con su correo electrónico y la nueva contraseña.
Depurar el correo electrónico
Si no se puede obtener el trabajo de correo electrónico:
Establecer un punto de interrupción en EmailSender.Execute comprobar SendGridClient.SendEmailAsync se
llama.
Crear un aplicación de consola para enviar correo electrónico mediante código similar a EmailSender.Execute .
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 diferentes (Microsoft, Yahoo,
Gmail, etcetera.)
Pruebe a enviar a las cuentas de correo electrónico diferente.
Una práctica recomendada de seguridad es no use secretos de producción en desarrollo y pruebas. Si
publica la aplicación en Azure, puede establecer los secretos de SendGrid como configuración de la aplicación en
el portal de Azure Web App. El sistema de configuración está configurado para leer las claves de las variables de
entorno.

Combinar las cuentas de inicio de sesión social y local


Para completar esta sección, primero debe habilitar a un proveedor de autenticación externo. Consulte Facebook,
Google y la autenticación de proveedor externo.
Puede combinar las 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 en primer lugar y luego agregar un inicio de sesión local.
Haga clic en el administrar vínculo. Tenga en cuenta la externa 0 (inicios de sesión sociales) asociado con esta
cuenta.

Haga clic en el vínculo a otro servicio de inicio de sesión y acepte 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 las cuentas. Es posible que desee que los
usuarios agreguen cuentas locales en caso de su servicio de autenticación de inicio de sesión social esté inactivo
o que es más probable hayan perdido el acceso a su cuenta de redes sociales.

Habilitar la confirmación de cuenta después de un sitio tiene usuarios


Habilitar confirmación de la cuenta en un sitio con usuarios bloquea todos los usuarios existentes. Los usuarios
existentes están bloqueados porque sus cuentas no confirmadas. 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 se ha confirmado.
Confirme que los usuarios existentes. Por ejemplo, envío por lotes de mensajes de correo electrónico con
vínculos de confirmación.
Habilitar la generación de código QR para
aplicaciones de authenticator TOTP en ASP.NET Core
22/08/2018 • 5 minutes to read • Edit Online

Códigos QR requiere ASP.NET Core 2.0 o posterior.


ASP.NET Core se distribuye con el soporte técnico para las aplicaciones de authenticator para la autenticación
individual. Dos aplicaciones de autenticador de autenticación (2FA) de factor, con una duración definida por única
vez contraseña algoritmo (TOTP ), son el enfoque para 2FA recomendado en el sector. 2FA uso TOTP es preferible
a SMS 2FA. Una aplicación authenticator 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 de autenticador está
instalada en un Smartphone.
Las plantillas de aplicación web de ASP.NET Core admiten autenticadores, pero no proporcionan compatibilidad
para la generación de CódigoQR. Los generadores de CódigoQR facilitan la instalación de 2FA. Este documento
le guiará a través de agregar código QR generación a la página de configuración 2FA.

Agregar códigos QR en la página de configuración de 2FA


Estas instrucciones usan qrcode.js desde el https://davidshimjs.github.io/qrcodejs/ repositorio.
Descargue el qrcode.js javascript library a la wwwroot\lib carpeta del proyecto.
Siga las instrucciones de Scaffold identidad para generar
/Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml.
En /Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml, busque el Scripts sección al final
del archivo:
En Pages/Account/Manage/EnableAuthenticator.cshtml (las páginas de Razor) o
Views/Manage/EnableAuthenticator.cshtml (MVC ), busque el 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. Debe tener el aspecto siguiente:

@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.
Ejecute 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 elige al crear inicialmente el proyecto.
Puede cambiar mediante la búsqueda de la GenerateQrCodeUri(string email, string unformattedKey) método en
el /Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml.
El nombre del sitio en el código QR se toma del nombre del proyecto que elige al crear inicialmente el proyecto.
Puede cambiar mediante la búsqueda de la GenerateQrCodeUri(string email, string unformattedKey) método en
el Pages/Account/Manage/EnableAuthenticator.cshtml.cs archivo (las páginas de Razor) o el
Controllers/ManageController.cs archivo (MVC ).
El código predeterminado de la plantilla tiene el aspecto siguiente:

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 del sitio procedente de su nombre de la


solución. Se puede cambiar a cualquier valor, pero siempre debe estar codificado como URL.

Uso de una biblioteca de código QR diferente


Puede reemplazar la biblioteca de código QR con la biblioteca preferida. El código HTML contiene un qrCode
elemento donde se puede colocar un código QR mediante cualquier mecanismo de la biblioteca proporciona.
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 desfase horario


Autenticación TOTP (basado en tiempo de contraseña de un solo uso) depende del dispositivo con el servidor y el
autenticador tiene una hora precisa. Los tokens solo duran 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
27/09/2018 • 8 minutes to read • Edit Online

Por Rick Anderson y suizo Devs

WARNING
Dos aplicaciones de autenticador de autenticación (2FA) de factor, con una duración definida por única vez contraseña
algoritmo (TOTP), son el enfoque para 2FA recomendado en el sector. 2FA uso TOTP es preferible a SMS 2FA. Para obtener
más información, consulte generación habilitar código QR para las aplicaciones de authenticator TOTP en 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) mediante SMS. Se proporcionan
instrucciones para twilio y ASPSMS, pero puede usar cualquier otro proveedor SMS. Se recomienda realizar
confirmación de la cuenta y contraseña de recuperación antes de comenzar este tutorial.
Ver el ejemplo completado. Cómo descargar.

Crear 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 ASP.NET Core para configurar y requieren SSL.
Crear una cuenta SMS
Crear una cuenta SMS, por ejemplo, desde twilio o ASPSMS. Registre las credenciales de autenticación (twilio:
accountSid y authToken para ASPSMS: Userkey y la contraseña).
Averiguar las credenciales del proveedor de SMS
Twilio: en la ficha Panel de la cuenta de Twilio, copie el SID de cuenta y Auth token.
ASPSMS: desde la configuración de su cuenta, vaya a Userkey y cópielo junto con su contraseña.
Más adelante, almacenaremos estos valores con la herramienta Administrador de secretos en el conjunto de
claves SMSAccountIdentification y SMSAccountPassword .
Especifica el Id. de remitente / originador
Twilio: desde la ficha números, copie su Twilio número de teléfono.
ASPSMS: en el menú de los originadores desbloquear, desbloquear uno o varios de los originadores o elija un
originador alfanumérico (no admitido todas las redes).
Más adelante se almacenará este valor con la herramienta secret manager dentro de la clave SMSAccountFrom .
Proporcione las credenciales para el servicio SMS
Vamos a usar el patrón de opciones para tener acceso a la configuración de cuenta y clave de usuario.
Cree una clase para capturar la clave segura de SMS. Para 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 secret


manager. Por ejemplo:

C:/Web2FA/src/WebApp1>dotnet user-secrets set SMSAccountIdentification 12345


info: Successfully saved SMSAccountIdentification = 12345 to the secret store.

Agregue el paquete NuGet del proveedor de SMS. Desde el Administrador de consola paquetes (PMC )
ejecute:
Twilio: Install-Package Twilio

ASPSMS: Install-Package ASPSMS

Agregue código en el Services/MessageServices.cs archivo para habilitar SMS. Utilice el Twilio o la sección
ASPSMS:
Twilio: [!code-csharp]
ASPSMS: [!code-csharp]
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 el Views/Manage/Index.cshtml archivo de vista de Razor y quite el comentario caracteres (de modo que no
hay ningún marcado commnted out).

Inicie sesión con autenticación en dos fases


Ejecute 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 administración. 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 verificación y pulse enviar código de verificación.
Obtendrá un mensaje de texto con el código de verificación. Escríbala y pulse enviar

Si no recibe un mensaje de texto, consulte la página de registro de twilio.


La vista de administración se 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 fases de prueba


Cierre la sesión.
Inicia sesión.
La cuenta de usuario habilitó la autenticación en dos fases, por lo que debe proporcionar el segundo factor
de autenticación. En este tutorial se ha habilitado la verificación por teléfono. Las plantillas integradas
permiten configurar el correo electrónico como segundo factor. Puede configurar factores adicionales de
segundo para la autenticación como códigos QR. Pulse enviar.

Escriba el código que se obtiene en el mensaje SMS.


Al hacer clic en el recordar este explorador casilla eximirán de la necesidad de usar 2FA para iniciar
sesión cuando se usa el mismo dispositivo y el explorador. Habilitar 2FA y haga clic en recordar este
explorador le proporcionará 2FA fuerte protección contra usuarios malintencionados que intenta acceder
a su cuenta, siempre que no tienen acceso a su dispositivo. Puede hacerlo en cualquier dispositivo privada
que se usan con frecuencia. Estableciendo recordar este explorador, obtendrá la seguridad añadida de
2FA desde dispositivos corrientemente no usamos 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
social, se almacena cada intento incorrecto en 2FA. Si se alcanza los intentos de acceso con error 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 máximo
intentos de acceso y se puede establecer el tiempo de bloqueo con MaxFailedAccessAttempts y
DefaultLockoutTimeSpan. El siguiente ejemplo configura el bloqueo de cuentas durante 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 de cookies sin ASP.NET Core
Identity
18/09/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, desea 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 en la aplicación de ejemplo, la cuenta de usuario para el usuario hipotética, María
Rodriguez, está codificado en la aplicación. Utilice el nombre de usuario de correo electrónico
"maria.rodriguez@contoso.com" y cualquier contraseña para iniciar sesión 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, el usuario
podría autenticarse en una base de datos.
Para obtener información sobre la migración de la autenticación basada en cookies de ASP.NET Core 1.x a 2.0,
consulte migrar autenticación e Identity a ASP.NET Core 2.0 tema (autenticación basada en cookies).
Para usar ASP.NET Core Identity, consulte el Introducción a Identity tema.

Configuración
ASP.NET Core 2.x
ASP.NET Core 1.x
Si la aplicación no usa el Microsoft.AspNetCore.App metapaquete, cree una referencia de paquete en el archivo
de proyecto para el Microsoft.AspNetCore.Authentication.Cookies paquete (versión 2.1.0 o más adelante).
En el ConfigureServices método, cree 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 la autenticación con cookies y desea
autorizar 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.
Esquema de autenticación de la aplicación es diferente de esquema de autenticación de cookies de la aplicación.
Cuando no se proporciona un esquema de autenticación de cookies para AddCookie, usa
CookieAuthenticationDefaults.AuthenticationScheme ("Cookies").
En el Configure método, use el UseAuthentication método para invocar 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 usa para configurar las opciones de proveedor de autenticación.

OPCIÓN DESCRIPCIÓN

AccessDeniedPath Proporciona la ruta de acceso para proporcionar un 302


encontrado (redirección de URL) cuando se desencadena
mediante HttpContext.ForbidAsync . El valor
predeterminado es /Account/AccessDenied .

ClaimsIssuer El emisor que se usará para la emisor propiedad en las


notificaciones creadas por el servicio de autenticación de
cookies.

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 solicitudes a un nombre de
host coincidente. Es posible que desee ajustar esta opción
para que las cookies disponibles para cualquier host en el
dominio. Por ejemplo, establecer el dominio de cookies en
.contoso.com pone a disposición contoso.com ,
www.contoso.com , y staging.www.contoso.com .

Cookie.Expiration Obtiene o establece la duración de una cookie. Actualmente,


esta opción no funciona y quedarán obsoleta en ASP.NET
Core 2.1 +. Use el 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 estar accesible sólo a
los servidores. Cambiar 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) 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 una aplicación que se ejecuta
en /app1 y desea restringir las cookies a esa aplicación,
establezca el CookiePath propiedad /app1 . Al hacerlo, solo
está disponible en las solicitudes a la cookie /app1 y
cualquier aplicación debajo de ella.
OPCIÓN DESCRIPCIÓN

Cookie.SameSite Indica si el explorador debe permitir la cookie que se


adjuntará a solo las solicitudes del mismo sitio (
SameSiteMode.Strict ) o solicitudes entre sitios mediante
los métodos HTTP seguros y las 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 proporcione. Para
admitir la autenticación de OAuth, el valor predeterminado es
SameSiteMode.Lax . Para obtener más información, consulte
autenticación OAuth interrumpida debido a la directiva de
cookies de SameSite.

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 usa para crear


el valor predeterminado TicketDataFormat . Si el
TicketDataFormat propiedad está establecida, el
DataProtectionProvider no se usa la opción. Si no se
proporciona, se usa 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 aplicaciones en ciertos puntos de
procesamiento. Si Events no siempre, se proporciona una
instancia predeterminada que no hace nada cuando se llama
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 que expira el vale de autenticación que


se almacena dentro de la cookie. ExpireTimeSpan se agrega
a la hora actual para crear la hora de expiración para el vale. El
ExpiredTimeSpan valor siempre se entra en el cifrado
AuthTicket verificada por el servidor. También podría entrar en
el Set-Cookie encabezado, pero solo si IsPersistent está
establecido. Para establecer IsPersistent a true ,
configure el AuthenticationProperties pasa a SignInAsync .
El valor predeterminado de ExpireTimeSpan es 14 días.

LoginPath Proporciona la ruta de acceso para proporcionar un 302


encontrado (redirección de URL) cuando se desencadena
mediante 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 que una solicitud para el
LoginPath una nueva identidad de inicio de sesión, se
concede el ReturnUrlParameter valor se utiliza para redirigir
el explorador a la dirección URL que ocasionó que el código
de estado sin autorización original. El valor predeterminado es
/Account/Login .
OPCIÓN DESCRIPCIÓN

LogoutPath Si el LogoutPath se proporciona al controlador, a


continuación, redirige una solicitud a esa ruta de acceso en
función del valor de la ReturnUrlParameter . El valor
predeterminado es /Account/Logout .

ReturnUrlParameter Determina el nombre del parámetro de cadena de consulta


que se anexa el controlador para una respuesta 302
encontrado (redirección de 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 usa para almacenar la identidad


a través de solicitudes. Cuando se utiliza, solo un identificador
de sesión se envía al cliente. SessionStore puede utilizarse
para mitigar posibles problemas con identidades de gran
tamaño.

slidingExpiration Una marca que indica si se debe emitir una cookie nueva con
un tiempo de expiración actualizada dinámicamente. Esto
puede ocurrir en cualquier solicitud donde el actual período
de expiración de cookie más del 50% expiró. La nueva fecha
de expiración se mueve hacia delante a la fecha actual más el
ExpireTimespan . Un tiempo de expiración absoluta cookie
puede establecerse mediante el AuthenticationProperties
al llamar a la clase SignInAsync . Un tiempo de expiración
absoluta puede mejorar la seguridad de la aplicación al limitar
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 comprueba 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 la directiva de cookies.


Middleware de cookies directiva habilita capacidades de directiva de cookies en 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 la directiva de cookies le permiten controlar


características globales del procesamiento de la cookie y el enlace en los controladores de procesamiento de la
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 estar 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 ASP.NET Core 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 Core 2.0 + solo)


El valor predeterminado MinimumSameSitePolicy es el valor 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 la interrumpe OAuth2 y otros esquemas de autenticación de
origen cruzado, eleva el nivel de seguridad de la cookie para otros tipos de aplicaciones que no dependen de
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 las notificacións y llamada 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 especifica un AuthenticationScheme ,
se usa el esquema predeterminado.
En segundo plano, el cifrado que usa es ASP.NET Core protección de datos sistema. Si se va a hospedar la
aplicación en varias máquinas, equilibrio de carga entre aplicaciones o usar una granja de servidores web,
entonces debe configurar la protección de datos para usar el mismo conjunto de claves y el identificador de
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 sus cookies, llame a SignOutAsync:

await HttpContext.SignOutAsync(
CookieAuthenticationDefaults.AuthenticationScheme);

Si no usa 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 usa 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 deshabilita a un usuario
en los sistemas de back-end, el sistema de autenticación de cookies no tiene ningún conocimiento de este, y un
usuario permanece abierta siempre y cuando 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 validación de la identidad de la cookie. Este enfoque reduce el riesgo de revocados
a los usuarios acceden 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 la cookie sigue siendo válida. Para implementar este
escenario, la base de datos, que se implementa en IUserRepository para este ejemplo, almacena una
LastChanged valor. Cuando se actualiza cualquier usuario en la base de datos, el LastChanged valor se establece
en la hora actual.
Con el fin de invalidar una cookie cuando los cambios de la base de datos según la LastChanged valor, cree la
cookie con un LastChanged de 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 la 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 del 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 una situación en la que se actualiza el nombre del usuario — una decisión que no afectan a la
seguridad de cualquier manera. Si desea actualizar sin destruir datos de la entidad de seguridad de usuario, 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 la
aplicación.

Cookies persistentes
Desea que la cookie para conservar entre sesiones del explorador. Esta persistencia solo debería habilitarse con el
consentimiento del usuario explícita con una casilla "Recordar mi cuenta" en el 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 del explorador. Se respeta cualquier configuración de expiración deslizante configurado previamente. Si la
cookie expira mientras se cierra el explorador, el explorador borrará 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 se establece en
SignInAsync , invalida el valor de la ExpireTimeSpan opción de CookieAuthenticationOptions , si establece.

El fragmento de código siguiente crea una identidad y cookie correspondiente que tiene una duración 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
Los cambios de autenticación 2.0 o anuncio de la migración
Autorizar con un esquema específico de ASP.NET Core
Autorización basada en notificaciones en ASP.NET Core
Comprobaciones de la función basada en directivas
Hospedaje de ASP.NET Core en una granja de servidores web
Azure Active Directory con ASP.NET Core
29/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
06/09/2018 • 10 minutes to read • Edit Online

Por Cam Soper


Azure B2C de Active Directory (Azure AD B2C ) es una solución de administración de identidades de nube para
aplicaciones web y móviles. El servicio proporciona autenticación para las aplicaciones hospedadas en la nube y
locales. Tipos de autenticación incluyen cuentas individuales, las cuentas de redes sociales y federados cuentas de
empresa. 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 las aplicaciones 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 B2C de Azure AD
Use 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 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 B2C de Azure AD


En el inquilino de Azure AD B2C recién creado, registre la aplicación mediante los pasos descritos en la
documentación de bajo el registrar una aplicación web sección. Detener el crear un secreto de cliente de
aplicación web sección. No es necesario un secreto de cliente para este tutorial.
Use los siguientes valores:

PARÁMETRO VALOR NOTAS


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 aplicación web o API web Sí

Permitir flujo implícito Sí

Dirección URL de respuesta https://localhost:44300/signin- Direcciones URL de respuesta son


oidc puntos de conexión donde Azure AD
B2C devolverá los tokens que solicita la
aplicación. Visual Studio proporciona la
dirección URL de respuesta para usar.
Por ahora, escriba
https://localhost:44300/signin-
oidc
para completar el formulario.

URI de Id. de aplicación Deje en blanco No es necesario para este tutorial.

Incluir a cliente nativo No

WARNING
Si la configuración de una URL de respuesta que no sean localhost, ser consciente de la 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
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. No hay más configuración después de crea la aplicación de ASP.NET Core.

Crear una aplicación ASP.NET Core en Visual Studio 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. Complete el formulario con los siguientes valores:

PARÁMETRO VALOR

Nombre de dominio <el nombre de dominio del inquilino B2C>


PARÁMETRO VALOR

Id. de aplicación <Pegue el identificador de aplicación desde el


Portapapeles>

Ruta de acceso de devolución de llamada <Use el valor predeterminado>

Directiva de registro 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 junto al vínculo 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 la aplicación B2C sigue abiertas. Cambiar temporal
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 los copió la URL de respuesta, use la dirección SSL desde la pestaña de depuración en las propiedades del proyecto web
y anexe la CallbackPath valor desde appsettings.json.

Configuración de directivas
Siga los pasos de la documentación de Azure AD B2C para crear una directiva de registro 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 de registro, y notificaciones de aplicación.
Mediante el ejecutar ahora para probar las directivas, como se describe en la documentación es opcional.

WARNING
Asegúrese de los nombres de directiva exactamente como se describe en la documentación de esas directivas utilizadas en el
Cambiar autenticación cuadro de diálogo de Visual Studio. Los nombres de directiva se pueden comprobar en
appsettings.json.

Ejecutar la aplicación
En Visual Studio, presione F5 para compilar y ejecutar la aplicación. Una vez que inicie la aplicación web,
seleccione Accept para aceptar el uso de cookies (si se le solicita) y, a continuación, seleccione inicie 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 Suscríbase 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 B2C de Azure AD
Use Visual Studio para crear una aplicación Web de ASP.NET Core configurados 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 Learning para:
Personalizar la interfaz de usuario de Azure AD B2C.
Configurar los requisitos de complejidad de contraseña.
Habilitar la autenticación multifactor.
Configurar proveedores de identidad adicional, como Microsoft, Facebook, Google, Amazon, Twitter y otros.
Usar la API de Azure AD Graph para recuperar información adicional del usuario, como la pertenencia al
grupo, desde el inquilino de Azure AD B2C.
Proteger un núcleo de ASP.NET web API mediante Azure AD B2C.
Llamar a una API web de .NET desde una aplicación web de .NET con Azure AD B2C.
Autenticación en la nube en web API con Azure
Active Directory B2C en ASP.NET Core
25/09/2018 • 16 minutes to read • Edit Online

Por Cam Soper


Azure B2C de Active Directory (Azure AD B2C ) es una solución de administración de identidades de nube para
aplicaciones web y móviles. El servicio proporciona autenticación para las aplicaciones hospedadas en la nube y
locales. Tipos de autenticación incluyen cuentas individuales, las cuentas de redes sociales y federados cuentas de
empresa. 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 las aplicaciones de confianza. Para obtener más información, consulte Azure AD B2C: preguntas más frecuentes
(P+F).

Dado que las API web no tienen ninguna interfaz de usuario, podemos 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 B2C de Azure AD.
Use 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.
Uso de Postman para simular una aplicación web que presenta un cuadro de diálogo de inicio de sesión,
recupera un token y lo usa para realizar una solicitud en la API web.

Requisitos previos
Se requiere para este tutorial lo siguiente:
Suscripción de Microsoft Azure
Visual Studio 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 registro o inicio de sesión


Siga los pasos de la documentación de Azure AD B2C para crear una directiva de registro 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 de registro, y notificaciones de 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 B2C de Azure AD


En el inquilino de Azure AD B2C recién creado, registre la API mediante los pasos descritos en la documentación
de bajo el registrar una API web sección.
Use 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 aplicación web o API web Sí

Permitir flujo implícito Sí

Dirección URL de respuesta https://localhost Direcciones URL de respuesta son


puntos de conexión donde Azure AD
B2C devolverá los tokens que solicita la
aplicación.

URI de Id. de aplicación API El URI no es necesario resolver 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 API en el inquilino. Seleccione la API que acaba de
registrar. Seleccione el copia icono a la derecha de la Id. de aplicación campo para copiarlo en el Portapapeles.
Seleccione ámbitos publicados y compruebe el valor predeterminado user_impersonation ámbito está presente.

Crear una aplicación ASP.NET Core en Visual Studio 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. Complete el formulario con los siguientes valores:

PARÁMETRO VALOR

Nombre de dominio <el nombre de dominio del inquilino B2C>

Id. de aplicación <Pegue el identificador de aplicación desde el


Portapapeles>

Directiva de registro 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 está decorada con el atributo Authorize, por lo que
todas las solicitudes requieren autenticación.

Ejecutar la API web


En Visual Studio, ejecute la API. Visual Studio inicia un explorador que señala a la 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 puede mostrar un error 404 de
(página no encontrado). Este es el comportamiento normal.

Uso de Postman para obtener un token y prueba de la API


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 en el inquilino de Azure AD B2C, debe
registrarse en el inquilino como una aplicación web. Registrar el uso de Postman los pasos descritos en la
documentación de bajo el registrar una aplicación web sección. Detener el crear un secreto de cliente de
aplicación web sección. No es necesario un secreto de cliente para este tutorial.
Use los siguientes valores:

PARÁMETRO VALOR NOTAS

Name Postman

Incluir aplicación web o API web Sí

Permitir flujo implícito Sí

Dirección URL de respuesta https://getpostman.com/postman

URI de Id. de aplicación <Deje en blanco> No es necesario para este tutorial.

Incluir a cliente nativo No

La aplicación web recién registrada 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 acceso a la API en el menú de la
izquierda.
2. Seleccione + agregar.
3. En el seleccionar API lista desplegable, seleccione el nombre de la API web.
4. En el seleccionar ámbitos 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, ya que se necesita para obtener un token de portador.
Crear una solicitud de Postman
Inicie a Postman. De forma predeterminada, Postman muestra la crear nuevo tras iniciar el cuadro de diálogo. Si
no se muestra el cuadro de diálogo, seleccione el + 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 en los tutoriales de ASP.NET Core botón.
Probar la API web sin autenticación
Para comprobar que la API web requiere autenticación, en primer lugar realice una solicitud sin autenticación.
1. En el escriba la dirección URL de solicitud , escriba la dirección URL de ValuesController . La dirección
URL es el mismo tal como se muestra en el explorador con api/values 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.
IMPORTANT
Si se produce un error "No se pudo obtener ninguna respuesta", es posible que deba deshabilitar comprobación del
certificado SSL en el configuración de Postman.

Obtener un token de portador


Para realizar una solicitud autenticada a la API web, se requiere un token de portador. Postman facilita iniciar
sesión 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 sigue:

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
PARÁMETRO VALOR NOTAS

Dirección URL de autenticación Reemplace <nombre_de_dominio_de


https://login.microsoftonline.com/tfp/<tenant
domain inquilino> con el nombre de
name>/B2C_1_SiUpIn/oauth2/v2.0/authorize
dominio del inquilino.

Id. de cliente <Escriba la aplicación Postman Id.


de aplicación>

Ámbito https://<tenant domain Reemplace <nombre_de_dominio_de


name>/<api>/user_impersonation inquilino> con el nombre de
openid offline_access
dominio del inquilino. Reemplace
<api> con el URI de Id. de aplicación
que asignó la API web cuando se
registra en primer lugar (en este
caso, api ). El patrón de la dirección
URL es:
https:// {tenant }.onmicrosoft.com/ {ap
i-id-uri}/ {scope nombre}.

Estado <Deje en blanco>

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 el
cuadro de diálogo. Inicie sesión con una cuenta existente (si se ha creado uno probar las directivas) o
seleccione Suscríbase 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, cierra la ventana 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. Esta vez, el estado de respuesta está 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 B2C de Azure AD.
Use 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.
Uso de Postman para simular una aplicación web que presenta un cuadro de diálogo de inicio de sesión,
recupera un token y lo usa para realizar una solicitud en la API web.
Seguir desarrollando su API de aprendizaje para:
Proteger un núcleo de ASP.NET en 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 los requisitos de complejidad de contraseña.
Habilitar la autenticación multifactor.
Configurar proveedores de identidad adicional, como Microsoft, Facebook, Google, Amazon, Twitter y otros.
Usar la API de Azure AD Graph para recuperar información adicional del usuario, como la pertenencia al
grupo, desde el inquilino de Azure AD B2C.
Artículos basados en los proyectos de ASP.NET Core
creados con cuentas de usuario individuales
21/09/2018 • 2 minutes to read • Edit Online

ASP.NET Core Identity se incluye en las plantillas de proyecto en Visual Studio con la opción "Cuentas de usuario
individuales".
Las plantillas de autenticación están disponibles en la CLI de .NET Core con -au Individual :

dotnet new mvc -au Individual


dotnet new webapi -au Individual
dotnet new webapp -au Individual

dotnet new mvc -au Individual


dotnet new webapi -au Individual
dotnet new razor -au Individual

Sin autenticación
La autenticación se especifica en la CLI de .NET Core con el -au opción. En Visual Studio, el Cambiar
autenticación cuadro de diálogo está disponible para nuevas aplicaciones web. El valor predeterminado para
nuevas aplicaciones web en Visual Studio es sin autenticación.
Proyectos creados con ninguna autenticación:
No contienen páginas web y la interfaz de usuario para iniciar sesión y cierre la sesión.
No contienen código de autenticación.

Autenticación de Windows
Se especifica la autenticación de Windows para las nuevas aplicaciones web en la CLI de .NET Core con el
-au Windows opción. En Visual Studio, el Cambiar autenticación cuadro de diálogo proporciona el Windows
autenticación opciones.
Si selecciona la autenticación de Windows, la aplicación está configurada para usar el módulo de autenticación de
Windows IIS. Autenticación de Windows está diseñada para sitios web de Intranet.

Recursos adicionales
Los artículos siguientes muestran cómo usar el código generado en las plantillas de ASP.NET Core que usan
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 ASP.NET Core con datos de usuario protegidos por autorización
Autorización en ASP.NET Core
02/07/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
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.
18/09/2018 • 28 minutes to read • Edit Online

Consulte este PDF para la versión de ASP.NET Core MVC. La versión 1.1 de ASP.NET Core de este tutorial se
encuentra en esto carpeta. La 1.1 de ejemplo de ASP.NET Core se encuentra en la ejemplos.
Consulte este pdf

Crear una aplicación ASP.NET Core con datos de usuario


protegidos por autorización
Por Rick Anderson y Joe Audette
Este tutorial muestra cómo crear una aplicación web ASP.NET Core con los datos protegidos por autorización del
usuario. Muestra una lista de contactos que autentican los usuarios (registrados) ha creado. Hay tres grupos de
seguridad:
Usuarios registrados puede ver todos los datos aprobados y pueden sus propios datos de editar o eliminar.
Los administradores de puede aprobar o rechazar los datos de contacto. Solo contactos aprobados son
visibles para los usuarios.
Los administradores puede aprobar o rechazar y editar o eliminar todos los datos.
En la siguiente imagen, el usuario Rick ( rick@example.com ) ha iniciado sesión. Rick solo puede ver los contactos
aprobados y editar/eliminar/crear nuevo vínculos para sus contactos. El último registro, creado por Rick,
muestra editar y eliminar vínculos. Otros usuarios no verán el último registro hasta un administrador o un
administrador cambia el estado a "Aprobado".
En la siguiente imagen, manager@contoso.com se registre y del rol managers:

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 administradores y administradores.


En la siguiente imagen, admin@contoso.com se registre y en la función Administradores:
El administrador tiene todos los privilegios. Puede leer, editar o eliminar cualquier contacto y cambiar el estado de
los contactos.
Se ha creado la aplicación por 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 garantiza que un usuario solo puede modificar sus datos.
ContactManagerAuthorizationHandler : Permite a los administradores aprobar o rechazar los contactos.
ContactAdministratorsAuthorizationHandler : Permite que los administradores pueden aprobar o rechazar los
contactos y editar o eliminar contactos.

Requisitos previos
En 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
En ASP.NET Core 2.1, User.IsInRole se produce un error cuando se usa AddDefaultIdentity . Este tutorial se usa
AddDefaultIdentity y, por tanto, requiere la versión preliminar de ASP.NET Core 2.2 1 o posterior. Consulte este
problema de GitHub para una solución alternativa.

El inicio y la aplicación completada


Descargar el completado app. Prueba la aplicación completa, por lo que se familiarice con sus características de
seguridad.
La aplicación de inicio
Descargar el starter app.
Ejecute 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
segura. Resultarle útil consultar el proyecto completado.
Vincular 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 de tabla en la identidad base de datos. El Status
campo determina si un contacto es visible para los usuarios en general.
Crear una nueva migración y actualización de la base de datos:
dotnet ef migrations add userID_Status
dotnet ef database update

Agregar servicios de función a la identidad


Anexar AddRoles para agregar servicios de rol:

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.AddDefaultIdentity<IdentityUser>().AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();

Requerir que los usuarios autenticados


Establezca la directiva de autenticación predeterminado para exigir que los usuarios se autentiquen:

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.AddDefaultIdentity<IdentityUser>().AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();

services.AddMvc(config =>
{
// using Microsoft.AspNetCore.Mvc.Authorization;
// using Microsoft.AspNetCore.Authorization;
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

Puede rechazar la autenticación en el nivel de método de página de Razor, controlador o acción con el
[AllowAnonymous] atributo. Configuración de la directiva de autenticación predeterminado para exigir que los
usuarios se autentiquen protege recién agregada de las páginas de Razor y controladores. Tener la autenticación
requerida por el valor predeterminado es más segura que confiar en los nuevos controladores y las páginas de
Razor debe incluir el [Authorize] atributo.
Agregar AllowAnonymous al índice, acerca de y póngase en contacto con páginas para los usuarios anónimos
pueden obtener información sobre el sitio antes de que se registren.
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace ContactManager.Pages
{
[AllowAnonymous]
public class IndexModel : PageModel
{
public void OnGet()
{

}
}
}

Configurar la cuenta de prueba


La SeedData clase crea dos cuentas: administrador y administrador. Use la herramienta Secret Manager para
establecer una contraseña para estas cuentas. Establecer la contraseña del directorio de proyecto (el directorio
que contiene Program.cs):

dotnet user-secrets set SeedUserPW <PW>

Si no se especifica una contraseña segura, se produce una excepción cuando SeedData.Initialize se llama.
Actualización Main usar la contraseña de la prueba:
public class Program
{
public static void Main(string[] args)
{
var host = CreateWebHostBuilder(args).Build();

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.Message, "An error occurred seeding the DB.");
}
}

host.Run();
}

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>


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

Crear las cuentas de prueba y actualizar los contactos


Actualización de la Initialize método en el 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 seed both with the same password.
// Password is set with the following:
// 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 managerID = await EnsureUser(serviceProvider, testUserPw, "manager@contoso.com");
await EnsureRole(serviceProvider, managerID, Constants.ContactManagersRole);

SeedDB(context, adminID);
}
}

private static async Task<string> EnsureUser(IServiceProvider serviceProvider,


string testUserPw, string UserName)
{
var userManager = serviceProvider.GetService<UserManager<IdentityUser>>();

var user = await userManager.FindByNameAsync(UserName);


if (user == null)
{
user = new IdentityUser { 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 (roleManager == null)
{
throw new Exception("roleManager null");
}

if (!await roleManager.RoleExistsAsync(role))
{
IR = await roleManager.CreateAsync(new IdentityRole(role));
}

var userManager = serviceProvider.GetService<UserManager<IdentityUser>>();

var user = await userManager.FindByIdAsync(uid);

IR = await userManager.AddToRoleAsync(user, role);

return IR;
}

Agregue el identificador de usuario administrador y ContactStatus a los contactos. Realice uno de los contactos
"Enviado" y un "rechazada". Agregue el Id. de usuario y el estado a 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 controladores de autorización de administrador, administrador y


propietario
Crear un clase en el autorización carpeta. El
ContactIsOwnerAuthorizationHandler
ContactIsOwnerAuthorizationHandler comprueba que el usuario que actúan 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<IdentityUser> _userManager;

public ContactIsOwnerAuthorizationHandler(UserManager<IdentityUser>
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. Se realizan correctamente si el usuario autenticado


actual es el propietario del contacto. Controladores de autorización con carácter general:
Devolver context.Succeed cuando se cumplen los requisitos.
Devolver Task.CompletedTask cuando no se cumplen los requisitos. Task.CompletedTask ni éxito o error—
permite que otros controladores de autorización ejecutar.
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 es necesario comprobar la operación que se pasa en el parámetro de
requisito.
Crear un controlador de autorización de administrador
Crear un ContactManagerAuthorizationHandler clase en el autorización carpeta. El
ContactManagerAuthorizationHandler comprueba que el usuario que actúan 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;
}
}
}

Cree un controlador de autorización de administrador


Crear un ContactAdministratorsAuthorizationHandler clase en el autorización carpeta. El
ContactAdministratorsAuthorizationHandler comprueba que el usuario que actúan en el recurso es un
administrador. Administrador puede realizar 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


Se deben registrar los servicios mediante Entity Framework Core para inserción de dependencias mediante
AddScoped. El ContactIsOwnerAuthorizationHandler utiliza ASP.NET Core identidad, que se basa en Entity
Framework Core. Registre los controladores con la colección de servicios para que estén disponibles para el
ContactsController a través de inserción de dependencias. Agregue el código siguiente al final de
ConfigureServices :
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.AddDefaultIdentity<IdentityUser>().AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();

services.AddMvc(config =>
{
// using Microsoft.AspNetCore.Mvc.Authorization;
// using Microsoft.AspNetCore.Authorization;
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

// Authorization handlers.
services.AddScoped<IAuthorizationHandler,
ContactIsOwnerAuthorizationHandler>();

services.AddSingleton<IAuthorizationHandler,
ContactAdministratorsAuthorizationHandler>();

services.AddSingleton<IAuthorizationHandler,
ContactManagerAuthorizationHandler>();
}

ContactAdministratorsAuthorizationHandler y ContactManagerAuthorizationHandler se agregan como singleton. Lo


singletons con estado 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 de Razor y agregue una clase de los requisitos de operaciones.
Revisión de la clase de los requisitos de las operaciones de contacto
Revise el ContactOperations clase. Esta clase contiene los requisitos que admite la aplicación:
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 de contactos


Cree una clase base que contiene los servicios usados en los contactos de las páginas de Razor. La clase base
coloca el 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<IdentityUser> UserManager { get; }

public DI_BasePageModel(
ApplicationDbContext context,
IAuthorizationService authorizationService,
UserManager<IdentityUser> userManager) : base()
{
Context = context;
UserManager = userManager;
AuthorizationService = authorizationService;
}
}
}
El código anterior:
Agrega el IAuthorizationService service acceda a los controladores de autorización.
Agrega la identidad UserManager service.
Agregue la ApplicationDbContext .
Actualizar el CreateModel
Actualizar el constructor del modelo de página create para usar el DI_BasePageModel clase base:

public class CreateModel : DI_BasePageModel


{
public CreateModel(
ApplicationDbContext context,
IAuthorizationService authorizationService,
UserManager<IdentityUser> userManager)
: base(context, authorizationService, userManager)
{
}

Actualización de la CreateModel.OnPostAsync método:


Agregue 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 el IndexModel
Actualización de la OnGetAsync método por lo que solo contactos aprobados se muestran a los usuarios
generales:
public class IndexModel : DI_BasePageModel
{
public IndexModel(
ApplicationDbContext context,
IAuthorizationService authorizationService,
UserManager<IdentityUser> 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 el EditModel
Agregar un controlador de autorización para comprobar que el usuario 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 el recurso debe ser un imperativo. Las
comprobaciones deben realizarse una vez que la aplicación tenga acceso al recurso, cargándolo en el modelo de
página o cargándola en el controlador de sí mismo. Con frecuencia acceder al recurso pasando la clave de
recurso.

public class EditModel : DI_BasePageModel


{
public EditModel(
ApplicationDbContext context,
IAuthorizationService authorizationService,
UserManager<IdentityUser> 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 el DeleteModel
Actualice el modelo de página 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<IdentityUser> 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 para los contactos que no se puede
modificar el usuario.
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 varios using instrucciones.


Actualización de la editar y eliminar vincula en Pages/Contacts/Index.cshtml por lo que sólo se representen a
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
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.*@
<dt>
@Html.DisplayNameFor(model => model.Contact.Email)
</dt>
<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ágina de detalles:


public class DetailsModel : DI_BasePageModel
{
public DetailsModel(
ApplicationDbContext context,
IAuthorizationService authorizationService,
UserManager<IdentityUser> 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();
}

var isAuthorized = User.IsInRole(Constants.ContactManagersRole) ||


User.IsInRole(Constants.ContactAdministratorsRole);

var currentUserId = UserManager.GetUserId(User);

if (!isAuthorized
&& currentUserId != Contact.OwnerID
&& Contact.Status != ContactStatus.Approved)
{
return new ChallengeResult();
}

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

Agregar o quitar un usuario a un rol


Consulte este problema para obtener información sobre:
Quita los privilegios de un usuario. Por ejemplo un usuario en una aplicación de chat de silencio.
Agregar privilegios a un usuario.

Probar la aplicación completada


Si la aplicación tiene contactos:
Eliminar todos los registros de la Contact tabla.
Reinicie la aplicación para inicializar la base de datos.
Registrar un usuario para la exploración de los contactos.
Es una manera fácil de probar la aplicación completa iniciar las tres diferentes exploradores (o versiones de
InPrivate o incognito). En un explorador, registre un nuevo usuario (por ejemplo, test@example.com ). Inicie sesión
con un usuario diferente en cada explorador. Compruebe las siguientes operaciones:
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 ver muestra aprobar y
rechazar botones.
Los administradores pueden aprobar o rechazar y editar o eliminar todos los datos.

USUARIO OPCIONES

test@example.com Puede poseen los datos de editar o eliminar

manager@contoso.com Pueden aprobar o rechazar y editar o eliminar datos de


poseer

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 su eliminación 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


Cree una aplicación de páginas de Razor denominada "ContactManager"
Creación de la aplicación con cuentas de usuario individuales.
Asígnele el nombre "ContactManager" para el espacio de nombres coincida con el espacio de nombres
utilizado en el ejemplo.
-uld Especifica LocalDB en lugar de SQLite

dotnet new webapp -o ContactManager -au Individual -uld

Agregar Models\Contact.cs:

Scaffold el Contact modelo.


Crear la migración inicial y actualizar la base de datos:
dotnet aspnet-codegenerator razorpage -m Contact -udl -dc ApplicationDbContext -outDir Pages\Contacts --
referenceScriptLibraries
dotnet ef database drop -f
dotnet ef migrations add initial
dotnet ef database update

Actualización de la ContactManager anclar en el Pages/_Layout.cshtml archivo:

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

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.
Llame a SeedData.Initialize desde Main :

public class Program


{
public static void Main(string[] args)
{
var host = CreateWebHostBuilder(args).Build();

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


{
var services = scope.ServiceProvider;

try
{
var context = services.GetRequiredService<ApplicationDbContext>();
context.Database.Migrate();
SeedData.Initialize(services, "not used");
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred seeding the DB.");
}
}

host.Run();
}

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>


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

Probar 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 explica con más detalle en las características
de seguridad, presentadas en este tutorial.
Autorización en ASP.NET Core: Simple, en roles, basada en notificaciones y personalizada
Autorización personalizada basada en directivas
Convenciones de autorización de las páginas de
Razor en ASP.NET Core
22/08/2018 • 7 minutes to read • Edit Online

Por Luke Latham


Una manera de controlar el acceso en 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 permite que los usuarios
anónimos tener acceso a páginas individuales o carpetas de páginas. Aplican las convenciones descritas en este
tema automáticamente los filtros de autorización para controlar el acceso.
Vea o descargue el código de ejemplo (cómo descargarlo)
La aplicación de ejemplo usa autenticación de cookies sin ASP.NET Core Identity. La cuenta de usuario para el
usuario hipotética, María Rodriguez, está codificado en la aplicación. Utilice el nombre de usuario de correo
electrónico "maria.rodriguez@contoso.com" y cualquier contraseña para iniciar sesión 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, el usuario podría autenticarse 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 igualmente 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 un AuthorizeFilter a la página
en la ruta de acceso 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 relativa de raíz de las páginas
de Razor 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 puede aplicarse a una clase de modelo de página con el [Authorize] atributo de filtro. Para
obtener más información, consulte atributo de filtro Authorize.

Requerir autorización para acceder a una carpeta de páginas


Use la AuthorizeFolder convención a través de AddRazorPagesOptions para agregar un AuthorizeFilter a todas
las páginas en una carpeta en la ruta de acceso 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.

Requerir autorización para acceder a una página de área


Use la AuthorizeAreaPage convención a través de AddRazorPagesOptions para agregar un AuthorizeFilter a la
página de área en la ruta de acceso especificada:

options.Conventions.AuthorizeAreaPage("Identity", "/Manage/Accounts");

El nombre de página es la ruta de acceso del archivo sin extensión relativa al directorio raíz de páginas para el
área especificada. Por ejemplo, el nombre de página para el archivo
Areas/Identity/Pages/Manage/Accounts.cshtml es cuentas/administración/.
Un AuthorizeAreaPage sobrecarga está disponible si tiene que especificar una directiva de autorización.

Requerir autorización para acceder a una carpeta de áreas


Use la AuthorizeAreaFolder convención a través de AddRazorPagesOptions para agregar un AuthorizeFilter a
todas las áreas en una carpeta en la ruta de acceso especificada:

options.Conventions.AuthorizeAreaFolder("Identity", "/Manage");

La ruta de acceso de carpeta es la ruta de acceso de la carpeta relativa al directorio raíz de las páginas del área
especificada. Por ejemplo, la ruta de carpeta para los archivos en áreas/identidad/páginas/administrar o es
/administrar.
Un AuthorizeAreaFolder 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 un
AllowAnonymousFilter a una página en la ruta de acceso 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 relativa de raíz de las páginas
de Razor sin una extensión y que contiene solo barras diagonales.

Permitir el acceso anónimo a la carpeta páginas


Use la AllowAnonymousToFolder convención a través de AddRazorPagesOptions para agregar un
AllowAnonymousFilter a todas las páginas en una carpeta en la ruta de acceso 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 sobre la combinación de 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, no a la inversa. 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")

Requerir 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
27/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 se combinan [AllowAnonymous] y cualquier
[Authorize] atributo, el [Authorize] se omiten los atributos. Por ejemplo, si aplica [AllowAnonymous] en el nivel de
controlador, cualquier [Authorize] se omiten los atributos en el mismo controlador (o en cualquier acción dentro de él).
Autorización basada en roles en ASP.NET Core
31/07/2018 • 4 minutes to read • Edit Online

Cuando se crea una identidad puede pertenecer a uno o varios roles. Por ejemplo, mujer puede pertenecer a los
roles de administrador y usuario mientras Scott solo puede pertenecer al rol de usuario. Cómo se crean y
administran estas funciones depende del almacén de respaldo del proceso de autorización. Las funciones se
exponen a los desarrolladores a través de la IsInRole método en el ClaimsPrincipal clase.

IMPORTANT
Este tema no es válido con páginas de Razor. Es compatible con las páginas de Razor IPageFilter y IAsyncPageFilter. Para
más información, vea Filter methods for Razor Pages (Métodos de filtrado para páginas de Razor).

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 dentro de un controlador, especificar las funciones que el usuario actual debe ser
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 son miembros 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 sólo puede tener acceso a los 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 todas las funciones
especificadas; 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 aplicando los 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 acceder al
controlador y el SetTime acción, pero solo los miembros de la Administrator rol puede tener acceso el 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


Requisitos del rol también se pueden expresar utilizando la sintaxis de directiva nueva, donde el desarrollador
registra una directiva en el inicio como parte de la configuración del 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, puede especificarlas como parámetros a la
RequireRole método:
options.AddPolicy("ElevatedRights", policy =>
policy.RequireRole("Administrator", "PowerUser", "BackupAdministrator"));

En este ejemplo, 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.
}
Proveedores personalizados de directiva de
autorización mediante IAuthorizationPolicyProvider
en ASP.NET Core
22/08/2018 • 9 minutes to read • Edit Online

Por Mike Rousos


Normalmente, cuando se usa autorización basada en directivas, las directivas se registran mediante una llamada a
AuthorizationOptions.AddPolicy como parte de la configuración del servicio de autorización. En algunos
escenarios, puede no ser posible (o deseable) para registrar todas las directivas de autorización de esta manera. En
esos casos, puede utilizar una personalizada IAuthorizationPolicyProvider para controlar cómo se proporcionan
las directivas de autorización.
Ejemplos de escenarios donde un personalizado IAuthorizationPolicyProvider puede resultar útil incluir:
Usar un servicio externo para proporcionar evaluación de directivas.
Con un intervalo amplio de directivas (para los números de sala diferente o edades, por ejemplo), por lo que no
tiene sentido agregar cada directiva de autorización individuales con un AuthorizationOptions.AddPolicy llamar.
Creación de directivas en tiempo de ejecución basándose en la información de origen de datos externo (por
ejemplo, una base de datos) o determinar los requisitos de autorización de forma dinámica mediante otro
mecanismo.
Ver o descargar el código de ejemplo desde el repositorio de GitHub de aspnet/AuthSamples. Descargue el
archivo ZIP de repositorio aspnet/AuthSamples. Descomprima el AuthSamples-master.zip archivo. Navegue hasta
la ejemplos/CustomPolicyProvider carpeta del proyecto.

Personalizar la recuperación de directiva


Las aplicaciones de ASP.NET Core usan una implementación de la IAuthorizationPolicyProvider interfaz para
recuperar las directivas de autorización. De forma predeterminada, DefaultAuthorizationPolicyProvider se registra
y se utiliza. DefaultAuthorizationPolicyProvider Devuelve las directivas de la AuthorizationOptions proporcionado
en un IServiceCollection.AddAuthorization llamar.
Puede personalizar este comportamiento mediante el registro de otro IAuthorizationPolicyProvider
implementación en la aplicación inserción de dependencias contenedor.
El IAuthorizationPolicyProvider interfaz contiene dos API:
GetPolicyAsync devuelve una directiva de autorización para un nombre concreto.
GetDefaultPolicyAsync devuelve la directiva de autorización de forma predeterminada (la directiva utilizada
para [Authorize] atributos sin una directiva especificada).
Mediante la implementación de estas dos API, puede personalizar cómo se proporcionan las directivas de
autorización.

Parametrizar autorizar el ejemplo de atributo


Un escenario donde IAuthorizationPolicyProvider es útil es personalizado para permitir [Authorize] atributos
cuyos requisitos dependen de un parámetro. Por ejemplo, en autorización basada en directivas documentación, un
edades ("AtLeast21") se utiliza la directiva como un ejemplo. Si las acciones de controlador diferente en una
aplicación deben estar disponibles para los usuarios de diferentes edades, podría ser útil tener muchas directivas
diferentes edades. En vez de registrar todas las diferentes edades directivas que la aplicación necesitará en
AuthorizationOptions , puede generar las directivas de forma dinámica con un personalizado
IAuthorizationPolicyProvider . Para hacer con las directivas más fácil, puede anotar las acciones con el atributo de
autorización personalizado como [MinimumAgeAuthorize(20)] .

Atributos de autorización personalizada


Las directivas de autorización se identifican por sus nombres. Personalizado MinimumAgeAuthorizeAttribute descrito
anteriormente, debe asignar argumentos en una cadena que puede usarse para recuperar la directiva de
autorización correspondiente. Puede hacerlo mediante la derivación de AuthorizeAttribute y realizar el Age
encapsulado propiedad el AuthorizeAttribute.Policy propiedad.

internal class MinimumAgeAuthorizeAttribute : AuthorizeAttribute


{
const string POLICY_PREFIX = "MinimumAge";

public MinimumAgeAuthorizeAttribute(int age) => Age = age;

// Get or set the Age property by manipulating the underlying Policy property
public int Age
{
get
{
if (int.TryParse(Policy.Substring(POLICY_PREFIX.Length), out var age))
{
return age;
}
return default(int);
}
set
{
Policy = $"{POLICY_PREFIX}{value.ToString()}";
}
}
}

Este tipo de atributo tiene un Policy cadena según el prefijo codificado de forma rígida ( "MinimumAge" ) y un
entero pasa a través del constructor.
Se puede aplicar a acciones en la misma manera que otras Authorize atributos salvo que toma un entero como
parámetro.

[MinimumAgeAuthorize(10)]
public IActionResult RequiresMinimumAge10()

IAuthorizationPolicyProvider personalizado
Personalizado MinimumAgeAuthorizeAttribute facilita a las directivas de autorización de solicitud para todas las
edades mínima deseada. El siguiente problema para resolver es asegurarse de que las directivas de autorización
están disponibles para todos esas diferentes edades. Aquí es donde un IAuthorizationPolicyProvider es útil.
Cuando se usa MinimumAgeAuthorizationAttribute , los nombres de directiva de autorización seguirá el patrón
"MinimumAge" + Age , por lo que personalizado IAuthorizationPolicyProvider debe generar directivas de
autorización por:
La antigüedad del nombre de la directiva de análisis.
Uso de AuthorizationPolicyBuilder para crear un nuevo AuthorizationPolicy
Agregar requisitos a la directiva según la edad con AuthorizationPolicyBuilder.AddRequirements . En otros
escenarios, puede usar RequireClaim , RequireRole , o RequireUserName en su lugar.

internal class MinimumAgePolicyProvider : IAuthorizationPolicyProvider


{
const string POLICY_PREFIX = "MinimumAge";

// Policies are looked up by string name, so expect 'parameters' (like age)


// to be embedded in the policy names. This is abstracted away from developers
// by the more strongly-typed attributes derived from AuthorizeAttribute
// (like [MinimumAgeAuthorize()] in this sample)
public Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
{
if (policyName.StartsWith(POLICY_PREFIX, StringComparison.OrdinalIgnoreCase) &&
int.TryParse(policyName.Substring(POLICY_PREFIX.Length), out var age))
{
var policy = new AuthorizationPolicyBuilder();
policy.AddRequirements(new MinimumAgeRequirement(age));
return Task.FromResult(policy.Build());
}

return Task.FromResult<AuthorizationPolicy>(null);
}
}

Varios proveedores de directiva de autorización


Al usar custom IAuthorizationPolicyProvider implementaciones, tenga en cuenta que ASP.NET Core solo usa una
instancia de IAuthorizationPolicyProvider . Si un proveedor personalizado no es capaz de proporcionar directivas
de autorización para todos los nombres de directiva, debe recurrir a un proveedor de copia de seguridad. Los
nombres de directiva podrían incluirlas que proceden de una directiva predeterminada para [Authorize] atributos
sin un nombre.
Por ejemplo, considere que una aplicación necesita directivas personalizadas de edad y la recuperación de
directivas basado en roles más tradicional. Este tipo de aplicación podría usar un proveedor de directivas de
autorización personalizado que:
Intenta analizar los nombres de directiva.
Las llamadas a un proveedor de directivas diferentes (como DefaultAuthorizationPolicyProvider ) si el nombre
de la directiva no contiene una edad.

Directiva predeterminada
Además de proporcionar directivas de autorización con nombre, un personalizado IAuthorizationPolicyProvider
debe implementar GetDefaultPolicyAsync para proporcionar una directiva de autorización para [Authorize]
atributos sin un nombre de directiva especificado.
En muchos casos, este atributo de autorización solo requiere un usuario autenticado, por lo que puede realizar la
directiva necesaria con una llamada a RequireAuthenticatedUser :

public Task<AuthorizationPolicy> GetDefaultPolicyAsync() =>


Task.FromResult(new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build());

Como ocurre con todos los aspectos de un personalizado IAuthorizationPolicyProvider , puede personalizar esto,
según sea necesario. En algunos casos:
No se pueden usar las directivas de autorización de forma predeterminada.
Recuperación de la directiva predeterminada se puede delegar en una acción de reserva
IAuthorizationPolicyProvider .

Usar un IAuthorizationPolicyProvider personalizado


Para usar directivas personalizadas de un IAuthorizationPolicyProvider , debe:
Registrar adecuado AuthorizationHandler tipos con inserción de dependencias (se describe en autorización
basada en directivas), al igual que con todos los escenarios de autorización basada en directivas.
Registrar personalizado IAuthorizationPolicyProvider tipo de colección de servicios de inyección de
dependencia de la aplicación (en Startup.ConfigureServices ) para reemplazar el proveedor de directivas
predeterminado.

services.AddTransient<IAuthorizationPolicyProvider, MinimumAgePolicyProvider>();

Un completo personalizado IAuthorizationPolicyProvider ejemplo está disponible en el repositorio de GitHub de


aspnet/AuthSamples.
Inserción de dependencias en controladores de
requisitos en ASP.NET Core
30/07/2018 • 2 minutes to read • Edit Online

Controladores de autorización deben estar registrados en la colección de servicios durante la configuración


(mediante inserción de dependencias).
Suponga que tiene un repositorio de reglas que desea evaluar dentro de un controlador de autorización y ese
repositorio se registró en la colección de servicios. La autorización se resuelva e insertar en el constructor.
Por ejemplo, si deseara usar 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;
}
}

¿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 estar registrados como singleton.
Autorización basada en recursos en ASP.NET Core
30/07/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 del autor. Solo el autor tiene permiso para actualizar el documento. Por lo
tanto, el documento se debe recuperar del almacén de datos antes de que comience la evaluación de autorización.
Evaluación de atributos se produce antes del enlace de datos y antes de la ejecución del controlador de página o
acción que se carga el documento. Por estos motivos, la autorización declarativa con un [Authorize] atributo no
es suficiente. 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 (descarga) para explorar las características descritas en este tema.
Crear una aplicación 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.

Usar la autorización imperativa


Autorización se implementa como un IAuthorizationService de servicio y se registra en la colección de servicios
dentro de la Startup clase. El servicio está disponible a través de inserción de dependencias a los 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: una acepta el recurso y el nombre de la
directiva y la otra acepta 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 se proteja en personalizada Document objeto. Un AuthorizeAsync


sobrecarga se invoca para determinar si el usuario actual está autorizado para editar el documento proporcionado.
Una directiva de autorización personalizada "EditPolicy" se incluye en la decisión. Consulte autorización basada en
directivas de Custom para obtener más información sobre la creación de directivas de autorización.
NOTE
El código siguiente, los ejemplos se supone que 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 de recursos


Escribir un controlador para autorización basada en recursos no es mucho más diferente que escribir un
controlador simple requisitos. 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 aspecto siguiente:
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 va a realizar decisiones basadas en los resultados de las operaciones CRUD (creación, lectura, actualización,
eliminación), use el OperationAuthorizationRequirement clase auxiliar. Esta clase le permite escribir un único
controlador en lugar de una clase individual para cada operación. Para ello, proporcione 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, con un 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 operativos, especifique la operación al invocar AuthorizeAsync en su
controlador de página o la acción. El ejemplo siguiente determina si el usuario autenticado tiene permiso para
visualizar el documento proporcionado.

NOTE
El código siguiente, los ejemplos se supone que 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 está autenticado, devolver ForbidResult informa a cualquier middleware de
autenticación con errores 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 en ASP.NET Core
MVC
30/07/2018 • 2 minutes to read • Edit Online

A menudo, un programador desea mostrar, ocultar o modificar una interfaz de usuario según la identidad del
usuario actual. Solo puede acceder al servicio de autorización en las vistas MVC a través de inserción de
dependencias. Para insertar el servicio de autorización en una vista de Razor, use el @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
comprobaría si 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
comprobaría si 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 de que la evaluación de directivas debe tomar en
consideración.

WARNING
No confíe en la visibilidad de alternancia de 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 completamente 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
20/09/2018 • 11 minutes to read • Edit Online

Las aplicaciones Web necesitan a menudo 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 criptográfica 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 el sustituto a largo plazo
para el <machineKey> elemento en ASP.NET 1.x - 4.x. Se diseñó para tratar muchos de los defectos de la pila
antigua criptográfico al tiempo que proporciona una solución para la mayoría de casos de uso de aplicaciones
modernas es probable que encuentre.

Declaración del problema


En pocas palabras la declaración del problema general en una sola frase: necesito 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 podría escribirse como "Necesito para el estado de confianza de ida y vuelta a través de un cliente que no se
confía".
El ejemplo canónico de esta es una cookie de autenticación o bearer token. El servidor genera un "Soy Groot y
tener permisos de xyz" del token y lo entrega al cliente. En el futuro, el cliente presentará ese token al 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, altera y).
Puesto que el estado persistente es de confianza del servidor, se prevé que este estado podría contener
información específica para el entorno operativo. Esto podría ser en forma de una ruta de acceso de archivo, un
permiso, un identificador u otra referencia indirecta o algún otro elemento de datos específicos del servidor. No
generalmente deba ser divulgado dicha información a un cliente de confianza. Por lo tanto, el segundo requisito:
confidencialidad.
Por último, dado que las aplicaciones modernas están divididas en componentes que hemos visto es que los
componentes individuales desea aprovechar las ventajas de este sistema sin tener en cuenta otros componentes
del sistema. Por ejemplo, si un componente de token de portador se usa esta pila, debe funcionar sin
interferencias de un mecanismo de anti-CSRF que también podría estar usando la misma pila. Por lo tanto, el
último requisito: aislamiento.
Podemos proporcionar aún más las restricciones con el fin de restringir el ámbito de nuestros requisitos. Se
supone que todos los servicios que funcionan en los sistemas de cifrado se confía por igual y que los datos no
necesitan generar o consuman fuera de los servicios de nuestro control directo. Además, se requieren que las
operaciones sean tan rápidas como sea posible, ya que cada solicitud al servicio web podría pasar por el sistema
de cifrado una o varias veces. Esto hace que la criptografía simétrica sea ideal para nuestro escenario y nos
podemos descuento criptografía asimétrica hasta que tal vez que lo necesite.

Filosofía de diseño
Comenzamos mediante la identificación de problemas con la pila existente. Una vez que tenemos, hemos
encuestadas el panorama de las soluciones existentes y concluye que no hay ninguna solución existente tenía las
capacidades que se busca. A continuación, diseñamos una solución basada en varios principios fundamentales.
El sistema debe ofrecer sencillez de configuración. Lo ideal es que el sistema sería sin configuración y los
desarrolladores podrían ponerlo en marcha. En situaciones donde los desarrolladores deben configurar un
aspecto específico (por ejemplo, el repositorio de claves), prestarse a hacer que esas configuraciones
específicas simple.
Ofrecen una sencilla API orientadas al consumidor. La API deben ser fácil de usar correctamente y difícil
de usar incorrectamente.
Los desarrolladores no deben aprender los principios de administración de claves. El sistema debe
controlar la selección del algoritmo y la vigencia de la clave en nombre del desarrollador. Lo ideal es que el
desarrollador ni tan siquiera debe tener acceso al material de clave sin procesar.
Las claves deben estar protegidas en reposo cuando sea posible. El sistema debe descubrir un mecanismo
de protección predeterminado adecuado y lo aplica automáticamente.
Con estos principios en mente, hemos desarrollado una sencilla fácil de usar pila de protección de datos.
Las 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 DPAPI de Windows CNG y Azure Rights
Management son más adecuados para el escenario de almacenamiento ilimitada, y tienen capacidades de
administración de claves seguro según corresponda. Es decir, no hay nada que prohíben un desarrollador del uso
de las API de protección de datos de ASP.NET Core para la protección a largo plazo de los datos confidenciales.

Audiencia
El sistema de protección de datos se divide en cinco paquetes principales. Destino de diversos aspectos de estas
API tres tipos de destinatarios principales;
1. El información general sobre la API de consumidor a los desarrolladores de aplicaciones y el marco de
destino.
"No quiero obtener información acerca de cómo funciona la pila o acerca de cómo esté configurada.
Simplemente quiero realizar una manera alguna operación tan simple como sea posible con gran
probabilidad de que usar la API correctamente."
2. El API de configuración los desarrolladores de aplicaciones y los administradores del sistema de destino.
"Necesito indicar al 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. Había limitado a situaciones excepcionales y ha experimentado, los desarrolladores de
seguridad compatible con el uso de estas API.
"Necesito reemplazar un componente completo dentro del sistema, ya que tengo requisitos de
comportamiento realmente únicos. Estoy dispuesto a aprender con escasa frecuencia utiliza partes de la
superficie de API con el fin de crear un complemento que cumple Mis requisitos."

Diseño del paquete


La pila de protección de datos consta de cinco paquetes.
Microsoft.AspNetCore.DataProtection.Abstractions contiene el IDataProtectionProvider y IDataProtector
interfaces para crear servicios de protección de datos. También contiene métodos de extensión útiles para
trabajar con estos tipos (por ejemplo, IDataProtector.Protect). Si el sistema de protección de datos se crea
una instancia en otro lugar y consumen la API, referencia
Microsoft.AspNetCore.DataProtection.Abstractions .

Microsoft.AspNetCore.DataProtection contiene la implementación del núcleo del sistema de protección de


datos, incluidas las operaciones criptográficas de core, administración de claves, configuración y
extensibilidad. Para crear una instancia del sistema de protección de datos (por ejemplo, éste se agrega a
un IServiceCollection) o hacer referencia a modificar o extender su comportamiento,
Microsoft.AspNetCore.DataProtection .

Microsoft.AspNetCore.DataProtection.Extensions contiene API adicionales que los desarrolladores


pueden resultar útiles pero que no pertenecen en el paquete principal. Por ejemplo, este paquete contiene
métodos de generador para crear una instancia del sistema de protección de datos para almacenar las
claves en una ubicación en el sistema de archivos sin la inserción de dependencias (consulte
DataProtectionProvider). También contiene métodos de extensión para limitar la duración de cargas
protegidas (consulte ITimeLimitedDataProtector).
Microsoft.AspNetCore.DataProtection.SystemWeb puede instalarse en una aplicación existente de 4.x
ASP.NET para redirigir su <machineKey> operaciones para usar la nueva pila de protección de datos de
ASP.NET Core. Para obtener más información, consulta Reemplace el elemento machineKey ASP.NET en
ASP.NET Core.
Microsoft.AspNetCore.Cryptography.KeyDerivation proporciona una implementación de la contraseña
PBKDF2 hash rutina y se puede usar los sistemas que deben administrar de forma segura las contraseñas
de usuario. Para obtener más información, consulta Hash de contraseñas en ASP.NET Core.

Recursos adicionales
Hospedaje de ASP.NET Core en una granja de servidores web
Introducción a las API de protección de datos en
ASP.NET Core
23/08/2018 • 3 minutes to read • Edit Online

Su sencilla y protección de datos consta de los pasos siguientes:


1. Crear datos de un protector de 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, como ASP.NET Core SignalR, ya que configuración el sistema
de protección de datos y agregarlo a un contenedor de servicios que puede acceder a través de la inserción de
dependencias. El ejemplo siguiente muestra cómo configurar un contenedor de servicios para la inserción de
dependencias 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!
*/

Al crear un protector debe proporcionar uno o varios cadenas de propósito. Una cadena de propósito proporciona
aislamiento entre los consumidores. Por ejemplo, un protector creado con una cadena de propósito de "green" no
poder desproteger los datos proporcionados por un protector de con un propósito de "púrpura".
TIP
Las 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 , usará esa referencia para varias llamadas a Protect y Unprotect .
Una llamada a Unprotect generará CryptographicException si la carga protegida no se puede comprobar o descifrar.
Algunos componentes que desee omitir los errores durante desproteger operaciones; un componente que lee las cookies de
autenticación podría controlar este error y tratar la solicitud como si no tuviera ninguna cookie en todo lugar producirá un
error en la solicitud completa. Los componentes que desean 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/08/2018 • 3 minutes to read • Edit Online

Puesto que un IDataProtector también es implícitamente un IDataProtectionProvider , con fines se pueden


encadenar juntas. En este sentido, provider.CreateProtector([ "purpose1", "purpose2" ]) es equivalente a
provider.CreateProtector("purpose1").CreateProtector("purpose2") .
Esto permite que 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 al componente SecureMessage
provider.CreateProtector("Contoso.Messaging.SecureMessage") una vez por adelantado y almacenar en caché el
resultado en una privada _myProvider campo. Protectores futuras, a continuación, se pueden crear mediante
llamadas a _myProvider.CreateProtector("User: username") , y estos protectores se utilizarían para proteger los
mensajes individuales.
Esto también puede voltear. Considere la posibilidad de una sola aplicación lógica que hospeda varios inquilinos
(un CMS parece razonable) y cada inquilino se pueden configurar con su propio sistema de administración de
autenticación y el estado. La aplicación paraguas tiene un proveedor de maestro único y llama a
provider.CreateProtector("Tenant 1") y provider.CreateProtector("Tenant 2") para proporcionar su propio
segmento aislado del sistema de protección de datos a cada inquilino. Los inquilinos, a continuación, podrían
derivar sus propios protectores individuales en función de sus propias necesidades, pero independientemente de
cuán duro intentan no pueden crear protectores que entren en conflicto con otro inquilino en el sistema.
Gráficamente, esto se representa como sigue.

WARNING
Esto supone el paraguas de controles de aplicación, las API que están disponibles para 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 podría simplemente leer el material de clave
maestra directamente y derivar cualquier subclaves que deseen.

El sistema de protección de datos usa en realidad una especie de varios inquilinos en su propia configuración
predeterminada de fábrica. De forma predeterminada material de claves maestra se almacena en la carpeta de
perfil de usuario de la cuenta de proceso de trabajo (o el registro de identidades del grupo de aplicaciones de IIS ).
Pero es realmente bastante habitual usar una sola cuenta para ejecutar varias aplicaciones y, por tanto, todas estas
aplicaciones terminaría compartir 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. Con este fin implícita sirve para aislar las aplicaciones individuales entre sí al tratar
de forma eficaz cada aplicación como un único inquilino en el sistema y el proceso de creación de protector es
idéntico a la imagen anterior.
Hash de contraseñas en ASP.NET Core
18/09/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 dependencias en el resto del sistema de protección de datos. Se pueden usar completamente por separado.
El origen existe junto con el código de protección de datos base para su comodidad.
Actualmente, el paquete ofrece un método KeyDerivation.Pbkdf2 que permite el hash de una contraseña
mediante la PBKDF2 algoritmo. Esta API es muy similar a .NET Framework existente Rfc2898DeriveBytes tipo,
pero hay tres diferencias importantes:
1. El KeyDerivation.Pbkdf2método admite el consumo de varias PRFs (actualmente HMACSHA1 , HMACSHA256 ,y
HMACSHA512 ), mientras que el 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,
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=
*/

Consulte la código fuente para 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
21/09/2018 • 18 minutes to read • Edit Online

Cuando se inicializa el sistema de protección de datos, se aplica configuración predeterminada según el


entorno operativo. Esta configuración es normalmente adecuada para las aplicaciones que se ejecutan en
una sola máquina. Hay casos donde un desarrollador puede querer cambiar la configuración
predeterminada:
La aplicación se reparte entre varias máquinas.
Por motivos de cumplimiento.
Para estos escenarios, el sistema de protección de datos ofrece una API de configuración completo.

WARNING
Al igual que los archivos de configuración, el conjunto de claves de protección de datos deben protegerse mediante
los permisos adecuados. Puede elegir cifrar las claves en reposo, pero esto no impide que los atacantes crear nuevas
claves. Por lo tanto, la seguridad de la aplicación se ve afectada. 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 desea proteger
los archivos de configuración. Por ejemplo, si decide almacenar su conjunto de claves en el disco, utilice permisos del
sistema de archivos. Asegúrese sólo la identidad bajo la que se ejecuta la aplicación web ha lectura, escritura y crear
el acceso a ese directorio. Si usa Azure Table Storage, 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 Azure Key Vault, configurar el sistema con ProtectKeysWithAzureKeyVault en
el Startup clase:

public void ConfigureServices(IServiceCollection services)


{
services.AddDataProtection()
.PersistKeysToAzureBlobStorage(new Uri("<blobUriWithSasToken>"))
.ProtectKeysWithAzureKeyVault("<keyIdentifier>", "<clientId>", "<clientSecret>");
}

Establecer la ubicación de almacenamiento del conjunto de claves (por ejemplo,


PersistKeysToAzureBlobStorage). Dado que una llamada a la ubicación que se debe establecer
ProtectKeysWithAzureKeyVault implementa un IXmlEncryptor que deshabilita la configuración de
protección automática de los datos, incluida la ubicación de almacenamiento del conjunto de claves. El
ejemplo anterior usa Azure Blob Storage para conservar el conjunto de claves. Para obtener más
información, consulte proveedores de almacenamiento de claves: Azure y Redis. También puede conservar
el conjunto de claves localmente con PersistKeysToFileSystem.
El keyIdentifier es el identificador de clave de almacén de claves utilizado 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, String, String, 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 ya no automáticamente cifra las claves en reposo, puesto
que desconoce 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");
}

En ASP.NET Core 2.1 o posterior, puede proporcionar un X509Certificate2 a ProtectKeysWithCertificate,


como cargar un certificado desde un archivo:

public void ConfigureServices(IServiceCollection services)


{
services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(@"\\server\share\directory\"))
.ProtectKeysWithCertificate(
new X509Certificate2("certificate.pfx", "password"));
}

Consulte clave de cifrado en reposo para obtener más ejemplos e información sobre los mecanismos de
cifrado de claves integrada.
UnprotectKeysWithAnyCertificate
En ASP.NET Core 2.1 o posterior, puede girar certificados y descifrar las claves en reposo mediante una
matriz de X509Certificate2 certificados con UnprotectKeysWithAnyCertificate:

public void ConfigureServices(IServiceCollection services)


{
services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(@"\\server\share\directory\"))
.ProtectKeysWithCertificate(
new X509Certificate2("certificate.pfx", "password"));
.UnprotectKeysWithAnyCertificate(
new X509Certificate2("certificate_old_1.pfx", "password_1"),
new X509Certificate2("certificate_old_2.pfx", "password_2"));
}

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 las aplicaciones entre sí, incluso si
comparte el mismo repositorio clave físico. Esto evita que las aplicaciones desde la comprensión de los
demás cargas protegidas. Para compartir las cargas protegidas entre 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 tener un escenario donde no desea que una aplicación para implementar automáticamente las
claves (y crear nuevas claves) como enfocar la expiración. Un ejemplo de esto podría ser configuradas en
una relación principal/secundario, donde solo la aplicación principal es responsable de interés de
administración de claves y las aplicaciones secundarias solo tienen una vista de solo lectura del anillo de
clave de aplicaciones. Las aplicaciones secundarias pueden configurarse para tratar el conjunto de claves
como de solo lectura mediante la configuración del 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 las aplicaciones entre sí, incluso si esas aplicaciones se ejecutan en la misma cuenta de
proceso de trabajo y están usando el mismo material de clave maestra. Esto es algo similar para el
modificador IsolateApps desde de System.Web <machineKey > elemento.
El mecanismo de aislamiento se funciona teniendo en cuenta cada aplicación en el equipo local como un
único inquilino, por lo tanto el IDataProtector ha modificado para cualquier aplicación incluye
automáticamente el identificador de aplicación como discriminador. 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
debe ser estable suponiendo que los entornos de IIS se configuran de forma similar en todos los
equipos en 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 restablecimientos — 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 ejecutan 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 de
repositorios de claves.
Si el sistema de protección de datos no se proporciona 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 aplicaciones, todas las
aplicaciones respaldadas por el mismo material de clave pueden compartir las cargas de siempre
proporcionen adecuado fines. Para proporcionar aislamiento de aplicaciones en este entorno, llame a la
SetApplicationName método en la configuración de objetos y proporcione un nombre único para cada
aplicación.

Cambio de los algoritmos con UseCryptographicAlgorithms


La pila de protección de datos permite cambiar el algoritmo predeterminado usado por las claves recién
generadas. La manera más sencilla de hacerlo es llamar a UseCryptographicAlgorithms desde 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.
Una llamada a UseCryptographicAlgorithms le permite especificar el algoritmo deseado de una lista
predefinida integrada. No es necesario preocuparse por 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, recurre 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 conjunto de claves. Solo afecta a las claves recién
generadas.

Especificación de algoritmos administrados personalizados


ASP.NET Core 2.x
ASP.NET Core 1.x
Para especificar algoritmos administrados personalizados, cree un
ManagedAuthenticatedEncryptorConfiguration instancia que apunta 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 hormigón, implementaciones (a través de un


constructor público sin parámetros) se pueden crear instancias de SymmetricAlgorithm y
KeyedHashAlgorithm, aunque el sistema casos especiales los algunos valores como typeof(Aes) para
mayor 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 de modo CBC con relleno PKCS #7. El KeyedHashAlgorithm debe tener un tamaño de la síntesis
de > = 128 bits, y debe ser compatible con claves de una longitud igual a la longitud del resumen del algoritmo
hash. El KeyedHashAlgorithm no es estrictamente necesaria para ser HMAC.

Especificar los algoritmos personalizados de CNG de Windows


ASP.NET Core 2.x
ASP.NET Core 1.x
Para especificar un algoritmo personalizado de CNG de Windows con cifrado CBC -modo de validación de
HMAC, cree un CngCbcAuthenticatedEncryptorConfiguration instancia que contiene la información
algorítmica:

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étricos debe tener una longitud de clave de > = 128 bits, un tamaño de
bloque de > = 64 bits, y debe admitir el cifrado de modo CBC con relleno PKCS #7. El algoritmo hash debe tener un
tamaño de la síntesis de > = 128 bits y deben admitir que se abre con el BCRYPT_ALG_controlar_HMAC_indicador
de marca. El *las propiedades del proveedor se pueden establecer en null para usar 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 de modo contador de
Galois/con la validación, cree un CngGcmAuthenticatedEncryptorConfiguration instancia que contiene la
información algorítmica:

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étricos debe tener una longitud de clave de > = 128 bits, un tamaño de
bloque de 128 bits exactamente, y debe admitir el cifrado de GCM. Puede establecer el
EncryptionAlgorithmProvider propiedad en null para usar 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 como 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 de las principales rutinas de cifrado y
descifrado. Consulte IAuthenticatedEncryptor en principales de extensibilidad de criptografía para obtener
más información.

Conservar las claves cuando se hospedan en un contenedor de


Docker
Cuando se hospedan en un Docker contenedor, las claves deben mantenerse en uno:
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 en host.
Un proveedor externo, como Azure Key Vault o Redis.

Vea también
DI no compatible con escenarios para la protección de datos en ASP.NET Core
Admite la directiva de todo el equipo de protección de datos en ASP.NET Core
Hospedaje de ASP.NET Core en una granja de servidores web
Administración de claves de protección de datos y
la duración en ASP.NET Core
18/07/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 está hospedada en Azure Apps, 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 conjunto de claves para 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, intercambio de ensayo y producción o usando A pruebas a/b, cualquier aplicación con la
protección de datos no podrá descifrar los datos almacenados mediante el conjunto de claves dentro
de la ranura anterior. Esto conduce a los usuarios que se están registrados fuera de una aplicación que
utiliza la autenticación de cookies estándar de ASP.NET Core, ya que usa la protección de datos para
proteger sus cookies. Si así lo desea llaveros independiente de la ranura, utilice un proveedor de anillo
de clave externa, como Azure Blob Storage, Azure Key Vault, un almacén de SQL, o de Redis cache.
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 mediante DPAPI.
3. Si la aplicación se hospeda en IIS, las claves se guardan en el registro HKLM en una clave del registro
especial que se incluye sólo la cuenta de proceso de trabajo. Las claves se cifran en reposo con DPAPI.
4. Si ninguna de estas condiciones coinciden, no se conservan las claves fuera del proceso actual. Cuando el
proceso se cierra, todos los genera las claves se pierden.
El desarrollador está siempre en control total y puede invalidar cómo y dónde se almacenan las claves. Las tres
primeras opciones anteriores deben proporcionar valores predeterminados adecuados para la mayoría de las
aplicaciones similar a cómo ASP.NET <machineKey > rutinas de la generación automática trabajado 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 solo se produce en situaciones
excepcionales.
Cuando se hospedan en un contenedor de Docker, se deberían conservar las claves en una carpeta que es un
volumen de Docker (un volumen compartido o un volumen montado en el host, que se conserva más allá de la
duración del contenedor) o en un proveedor externo, como Azure Key Vault o Redis. Un proveedor externo
también es útil en escenarios de granja de servidores web si las aplicaciones no pueden acceder a un volumen
compartido de red (consulte PersistKeysToFileSystem para obtener más información).
WARNING
Si el desarrollador reemplaza las reglas descritas anteriormente y señala el sistema de protección de datos en un
repositorio específico de claves, cifrado automático de las claves en reposo está deshabilitado. Protección de reposo 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
permanecen en el sistema, siempre y cuando la aplicación puede descifrar los datos protegidos con ellos.
Consulte 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 confidencialidad y
HMACSHA256 autenticidad. Una clave maestra de 512 bits, puede cambiada cada 90 días, se usa para derivar
las claves secundarias dos utilizadas para estos algoritmos en una base por cada carga. Consulte derivación de
subclave para obtener más información.

Recursos adicionales
Extensibilidad de administración de claves en ASP.NET Core
Hospedaje de ASP.NET Core en una granja de servidores web
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
25/07/2018 • 14 minutes to read • Edit Online

TIP
Leer el administración de claves sección antes de leer esta sección, ya que explica algunos de los conceptos fundamentales
de estas API.

WARNING
Tipos que implementan cualquiera de las interfaces siguientes deben ser seguro para subprocesos para varios de los
llamadores.

Key
El IKey interfaz es la representación básica de una clave en los sistemas de cifrado. La clave de término se utiliza
aquí en el sentido abstracto, no en el sentido de "material criptográfico clave" literal. Una clave tiene las siguientes
propiedades:
Fechas de expiración, la creación y activación
Estado de revocación
Identificador de clave (GUID )
Además, IKey expone un CreateEncryptor método que se puede usar para crear un IAuthenticatedEncryptor
instancia asociado a esta clave.
Además, IKey expone un CreateEncryptorInstance 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 procesar de un IKey instancia.

IKeyManager
El IKeyManager interfaz representa un objeto responsable de almacenamiento de claves general, la recuperación y
manipulación. Expone tres operaciones de alto nivel:
Cree una nueva clave y enviarlo al 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 un IKeyManager una tarea muy avanzada y la mayoría de los desarrolladores no debería intentar realizar. En su
lugar, la mayoría de los desarrolladores deben sacar partido de las funciones que ofrece el XmlKeyManager clase.

XmlKeyManager
El XmlKeyManager es de tipo de la implementación concreta en el cuadro de IKeyManager . Proporciona varias
utilidades útiles, incluida 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 transcurso de cumplimiento de sus tareas:
AlgorithmConfiguration , que dicta los algoritmos usados por las nuevas claves.
IXmlRepository , que controla donde las claves se conservan en almacenamiento.
IXmlEncryptor [opcional], lo que permite cifrar las claves en reposo.
IKeyEscrowSink [opcional], que proporciona servicios de custodia de clave.

IXmlRepository , que controla donde las claves se conservan en almacenamiento.


IXmlEncryptor [opcional], lo 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 estos componentes se conectan entre sí
dentro de XmlKeyManager .

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 conserva en el documento sin cifrar el
IXmlRepository .)
Creación de clave / CreateNewKey
En la implementación de CreateNewKey , IAuthenticatedEncryptorConfiguration 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 conserva en el documento sin
cifrar el IXmlRepository .)

Recuperación de clave / GetAllKeys


En la implementación de GetAllKeys , el XML que representa claves documenta y se leen las revocaciones de
subyacente IXmlRepository . Si estos documentos están cifrados, el sistema les descifrará automáticamente.
XmlKeyManager crea la adecuada 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 con formato de
almacenamiento de claves.

IXmlRepository
El IXmlRepository interfaz representa un tipo que puede conservar XML y recuperar el XML desde un almacén de
respaldo. Expone dos API:
GetAllElements : IReadOnlyCollection<XElement>
StoreElement(XElement element, string friendlyName)

Las implementaciones de IXmlRepository no es necesario analizar el XML que se pasa a través de ellos. Deben
tratar los documentos XML como opaco y permitir que los niveles superiores preocuparse de generar y analizar
los documentos.
Hay cuatro tipos concretos integrados que implementan IXmlRepository :
FileSystemXmlRepository
RegistryXmlRepository
AzureStorage.AzureBlobXmlRepository
RedisXmlRepository
Consulte la documento de proveedores de almacenamiento de claves para obtener más información.
Registrar un personalizado IXmlRepository es adecuado cuando se usa un almacén de respaldo diferentes (por
ejemplo, Azure Table Storage).
Para cambiar el repositorio predeterminado de toda la aplicación, registrar un personalizado IXmlRepository
instancia:

services.Configure<KeyManagementOptions>(options => options.XmlRepository = new MyCustomXmlRepository());

services.AddSingleton<IXmlRepository>(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 serializador IAuthenticatedEncryptorDescriptor contiene todos los elementos marcados como "requiere
cifrado", a continuación, XmlKeyManager ejecutará esos elementos a través de la configurada IXmlEncryptor del
Encrypt método y conservarán el elemento cifrado en lugar de elemento de texto simple a la IXmlRepository . La
salida de la Encrypt método es un EncryptedXmlInfo objeto. Este objeto es un contenedor que contiene tanto el
resultante cifrado XElement y el tipo que representa un IXmlDecryptor que puede utilizarse para descifrar el
elemento correspondiente.
Hay cuatro tipos concretos integrados que implementan IXmlEncryptor :
CertificateXmlEncryptor
DpapiNGXmlEncryptor
DpapiXmlEncryptor
NullXmlEncryptor
Consulte la cifrado de claves en el documento de rest para obtener más información.
Para cambiar el mecanismo predeterminado de la clave de cifrado en reposo toda la aplicación, registrar un
personalizado IXmlEncryptor instancia:

services.Configure<KeyManagementOptions>(options => options.XmlEncryptor = new MyCustomXmlEncryptor());

services.AddSingleton<IXmlEncryptor>(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 información confidencial.
Recuerde que los descriptores de serializado podrían contener información confidencial (por ejemplo, el material
criptográfico) y esto es lo que llevó a la introducción de la IXmlEncryptor escriba en primer lugar. Sin embargo, los
accidentes suceden y llaveros puede ser eliminados o dañados.
La interfaz de custodia proporciona un sombreado de escape de emergencia, que permite el acceso al XML
serializado sin procesar antes de que se transforme alguno configurado IXmlEncryptor. La interfaz expone una
única API:
Store (keyId Guid, elemento de XElement)
Es hasta el 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 con un certificado X.509 corporativo conocido que se ha custodiado clave privada del certificado;
el CertificateXmlEncryptor tipo puede ayudar con esto. El IKeyEscrowSink implementación también es
responsable de conservar el elemento proporcionado de forma adecuada.
De forma predeterminada no está habilitado ningún mecanismo de custodia, aunque los administradores de
servidor pueden configurar esto globalmente. También se 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 sobrecargas de método la IServiceCollection.AddSingleton y
IServiceCollection.AddInstance sobrecargas, como IKeyEscrowSink instancias están pensadas para formar
singletons con estado. Si hay varios IKeyEscrowSink se registran las instancias, cada uno de ellos se llamará
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 que el material de clave sea accesible a una autoridad de
confianza y, puesto que la aplicación propia no es una autoridad de confianza, no debería tener acceso a su propio
material custodiada.
Ejemplo de código siguiente muestra cómo crear y registrar un IKeyEscrowSink donde se puede custodiar claves
tal que solo los miembros del "CONTOSODomain Admins" pueden recuperarlos.

NOTE
Para ejecutar este ejemplo, debe estar en un dominio de Windows 8 / 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 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
25/07/2018 • 12 minutes to read • Edit Online

El sistema de protección de datos administra automáticamente la duración de las claves maestras de usa para
proteger y desproteger cargas. Cada clave puede existir en uno de cuatro fases:
Se ha creado: la clave existe en el conjunto de claves, pero aún no se ha activado. La clave no debe usarse
para nuevas operaciones de protección hasta que haya transcurrido suficiente tiempo que la clave ha
tenido la oportunidad de propagarse a todos los equipos que usan este conjunto de claves.
Activo - en la clave existe en el conjunto de claves y se debe usar para todas las operaciones de proteger
de nuevo.
Ha caducado: la clave de su duración natural ha ejecutado y ya no se debe usar para las operaciones de
protección nuevo.
Revocar - la clave está en peligro y no debe usarse para nuevas operaciones de protección.
Las claves creadas, activas y caducadas pueden utilizarse para desprotección de cargas entrantes. Revocadas
claves de forma predeterminada no pueden usarse para desprotección de cargas, pero el desarrollador de
aplicaciones puede invalidar este comportamiento si es necesario.

WARNING
El desarrollador podría verse tentado a eliminar una clave desde el conjunto de claves (por ejemplo, eliminando el archivo
correspondiente del sistema de archivos). En ese momento, todos los datos protegidos por la clave es indescifrables de
forma permanente y no hay ninguna invalidación de emergencia como ocurre con las claves revocadas. Eliminación de una
clave es el comportamiento realmente destructivo, y por lo tanto el sistema de protección de datos no expone ninguna
API de primera clase para realizar esta operación.

Selección de la clave predeterminada


Cuando el sistema de protección de datos lee el conjunto de claves desde el repositorio de respaldo, intentará
encontrar una clave de "default" desde el conjunto de claves. 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. (Hay un factor aglutinante pequeño para permitir el reloj del servidor a
servidor sesgo). Si se ha expirado o se revoca la clave de generación de claves y si la aplicación no ha
deshabilitado automática, a continuación, se generará una nueva clave con la activación de inmediata por el
caducidad y gradual clave directiva siguiente.
El motivo por el sistema de protección de datos genera una nueva clave inmediatamente en lugar de recurrir a
una clave diferente es que la nueva generación de claves debe tratarse como una fecha de expiración implícito
de todas las claves que se han activado antes de la nueva clave. La idea general es que nuevas claves se han
configurado con algoritmos diferentes o mecanismos de cifrado en reposo que las claves antiguas, y el sistema
debería preferir al Revirtiendo 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,
preferentemente a las claves que han tenido tiempo en propagarse a otros equipos del clúster. El sistema de
reserva puede acabar elegir una clave expirada predeterminada como resultado. El sistema de reserva nunca se
elegirá una clave revocada como clave predeterminada y, si el conjunto de claves 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, automáticamente ha proporcionado una fecha de activación de {ahora + 2 días} y una
fecha de expiración de {ahora + 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 al almacén de respaldo
observar la clave en su siguiente período de actualización automática, lo que maximiza las posibilidades de que
cuando la clave de anillo activa hace que se convierten en se haya propagado a todas las aplicaciones que
pueden necesitar para usarlo.
Si la clave predeterminada expirará dentro de 2 días y el conjunto de claves 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 conjunto de claves. 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 {ahora + 90 días}. Esto permite al
sistema implementar automáticamente las claves de forma periódica se produzca ninguna interrupción del
servicio.
Puede haber circunstancias donde se creará una clave con la activación de inmediata. Un ejemplo sería cuando
la aplicación no se ha ejecutado durante un tiempo y todas las claves en el conjunto de claves se ha expirado.
Cuando esto sucede, la clave se proporciona una fecha de activación de {ahora} sin provocar el retraso de
activación de 2 días normal.
La vigencia de clave predeterminado es 90 días, aunque esto es configurable 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. Duración de la clave
predeterminada no puede ser inferior a 7 días.

Actualización automática de conjunto de claves


Cuando se inicializa el sistema de protección de datos, lee el conjunto de claves desde el repositorio subyacente
y lo almacena en caché en memoria. Esta caché permite proteger y desproteger operaciones podrán continuar
sin llegar a la memoria auxiliar. El sistema comprobará automáticamente el almacén de respaldo para cambios
aproximadamente cada 24 horas o cuando caduca la clave predeterminada actual, lo que ocurra primero.

WARNING
Los desarrolladores deberían con poca frecuencia (si alguna vez) es necesario usar directamente las API de administración
de clave. 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 conjunto de claves. El sistema de DI que proporciona la instancia de
IDataProtectionProvider también puede proporcionar una instancia de IKeyManager su consumo. Como
alternativa, puede extraer el IKeyManager directamente desde el IServiceProvider como se muestra en el
ejemplo siguiente.
Cualquier operación que modifica el conjunto de claves (crear una nueva clave de forma explícita 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 vuelva a leer el conjunto de claves y volver a crear la memoria caché.
El ejemplo siguiente muestra cómo utilizar el IKeyManager interfaz para inspeccionar y manipular el conjunto de
claves, 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.
* 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 un método heurístico mediante el cual intenta deducir una ubicación de
almacenamiento de claves adecuado y el mecanismo de cifrado en reposo automáticamente. El mecanismo de
persistencia de clave también es configurable por el desarrollador de aplicaciones. Los siguientes documentos
describen las implementaciones en el cuadro de estos mecanismos:
Proveedores de almacenamiento de claves en ASP.NET Core
Cifrado de claves en reposo en ASP.NET Core
Proveedores de almacenamiento de claves en
ASP.NET Core
27/09/2018 • 5 minutes to read • Edit Online

El sistema de protección de datos emplea un mecanismo de detección predeterminada para determinar dónde
se deben conservar las claves criptográficas. El desarrollador puede invalidar el mecanismo de detección
predeterminado y especificar manualmente la ubicación.

WARNING
Si especifica una ubicación de persistencia de clave explícita, el sistema de protección de datos anula el registro el cifrado
de clave predeterminado en el mecanismo de rest, por lo que las claves ya no se cifran en reposo. Se recomienda que,
además especifica un mecanismo de cifrado de claves explícitas para las implementaciones de producción.

Sistema de archivos
Para configurar un repositorio clave basada en el sistema de archivos, llame a la PersistKeysToFileSystem
rutina de configuración como se muestra a continuación. Proporcione un DirectoryInfo que apunta al
repositorio donde se deben almacenar las claves:

public void ConfigureServices(IServiceCollection services)


{
services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(@"c:\temp-keys\"));
}

El DirectoryInfo puede apuntar a un directorio en el equipo local, o puede señalar a una carpeta en un recurso
compartido de red. Si señala a un directorio en el equipo local (y el escenario es que solo las aplicaciones en el
equipo local requieren acceso a usar este repositorio), considere el uso de DPAPI de Windows (en Windows)
para cifrar las claves en reposo. De lo 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 claves de protección de datos en Azure Storage o en una caché en Redis. Las claves se
pueden compartir entre varias instancias de una aplicación web. Las aplicaciones pueden compartir las cookies
de autenticación o la protección de CSRF en varios servidores. Para configurar el proveedor de
almacenamiento de blobs de Azure, llame a uno de los PersistKeysToAzureBlobStorage sobrecargas:

public void ConfigureServices(IServiceCollection services)


{
services.AddDataProtection()
.PersistKeysToAzureBlobStorage(new Uri("<blob URI including SAS token>"));
}

Para configurar Redis, llame a uno de los PersistKeysToRedis sobrecargas:


public void ConfigureServices(IServiceCollection services)
{
var redis = ConnectionMultiplexer.Connect("<URI>");
services.AddDataProtection()
.PersistKeysToRedis(redis, "DataProtection-Keys");
}

Para obtener más información, vea los temas siguientes:


StackExchange.Redis ConnectionMultiplexer
Azure Redis Cache
ejemplos de ASPNET/DataProtection

Registro
Solo se aplica a las implementaciones de Windows.
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 (como w3wp.exede identidad del grupo de
aplicación). En estos casos, el administrador puede aprovisionar una clave del registro que es accesible
mediante la identidad de la cuenta de servicio. Llame a la PersistKeysToRegistry método de extensión tal como
se muestra a continuación. Proporcione un RegistryKey apunta a la ubicación donde se deben almacenar las
claves criptográficas:

public void ConfigureServices(IServiceCollection services)


{
services.AddDataProtection()
.PersistKeysToRegistry(Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Sample\keys"));
}

IMPORTANT
Se recomienda usar DPAPI de Windows para cifrar las claves en reposo.

Entity Framework Core


El Microsoft.AspNetCore.DataProtection.EntityFrameworkCore paquete proporciona un mecanismo para
almacenar las claves de protección de datos a una base de datos mediante Entity Framework Core. El
Microsoft.AspNetCore.DataProtection.EntityFrameworkCore paquete NuGet debe agregarse al archivo de
proyecto, no es parte de la Microsoft.AspNetCore.App metapaquete.
Con este paquete, las claves se pueden compartir entre varias instancias de una aplicación web.
Para configurar el proveedor de EF Core, llame a la PersistKeysToDbContext<TContext> método:
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")));

// using Microsoft.AspNetCore.DataProtection;
services.AddDataProtection()
.PersistKeysToDbContext<MyKeysContext>();

services.AddDefaultIdentity<IdentityUser>()
.AddDefaultUI(UIFramework.Bootstrap4)
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}

El parámetro genérico, TContext , debe heredar de DbContext y IDataProtectionKeyContext:

using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using WebApp1.Data;

namespace WebApp1
{
class MyKeysContext : DbContext, IDataProtectionKeyContext
{
// A recommended constructor overload when using EF Core
// with dependency injection.
public MyKeysContext(DbContextOptions<ApplicationDbContext> options)
: base(options) { }

// This maps to the table that stores keys.


public DbSet<DataProtectionKey> DataProtectionKeys { get; set; }
}
}

Repositorio de claves personalizado


Si los mecanismos en el equipo no son adecuados, el desarrollador puede especificar su propio mecanismo de
persistencia de clave al proporcionar una personalizada IXmlRepository.
Cifrado de claves en reposo en ASP.NET Core
25/07/2018 • 6 minutes to read • Edit Online

El sistema de protección de datos emplea un mecanismo de detección predeterminada para determinar las
claves criptográficas cómo se cifren en reposo. El desarrollador puede invalidar el mecanismo de detección y
especificar manualmente cómo se deben cifrar las claves en reposo.

WARNING
Si especifica una explícita ubicación de persistencia de la clave, el cifrado de clave predeterminado en el mecanismo de
rest anula el registro del sistema de protección de datos. Por lo tanto, las claves ya no se cifran en reposo. Se
recomienda especifica un mecanismo de cifrado de claves explícitas para las implementaciones de producción. En este
tema, se describen las opciones de mecanismo de cifrado en reposo.

Azure Key Vault


Para almacenar las claves en Azure Key Vault, configurar el sistema con ProtectKeysWithAzureKeyVault en el
Startup clase:

public void ConfigureServices(IServiceCollection services)


{
services.AddDataProtection()
.PersistKeysToAzureBlobStorage(new Uri("<blobUriWithSasToken>"))
.ProtectKeysWithAzureKeyVault("<keyIdentifier>", "<clientId>", "<clientSecret>");
}

Para obtener más información, consulte configurar protección de datos de ASP.NET Core:
ProtectKeysWithAzureKeyVault.

DPAPI de Windows
Solo se aplica a las implementaciones de Windows.
Cuando se utiliza DPAPI de Windows, el material de clave se cifra con CryptProtectData antes de que se
conservan en el almacenamiento. DPAPI es un mecanismo de cifrado adecuado para los datos que no se leen
nunca fuera de la máquina actual (aunque es posible realizar una copia de estas claves hasta que Active
Directory; vea DPAPI y perfiles móviles). Para configurar el cifrado de claves en reposo DPAPI, llame a uno de
los ProtectKeysWithDpapi métodos de extensión:

public void ConfigureServices(IServiceCollection services)


{
// Only the local user account can decrypt the keys
services.AddDataProtection()
.ProtectKeysWithDpapi();
}

Si ProtectKeysWithDpapi se llama sin parámetros, solo la cuenta de usuario de Windows actual puede
descifrar el conjunto de claves persistente. Opcionalmente, puede especificar que cualquier cuenta de usuario
en el equipo (no solo la cuenta de usuario actual) podrá descifrar el conjunto de claves:
public void ConfigureServices(IServiceCollection services)
{
// All user accounts on the machine can decrypt the keys
services.AddDataProtection()
.ProtectKeysWithDpapi(protectToLocalMachine: true);
}

Certificado X.509
Si la aplicación se reparte entre varias máquinas, puede ser conveniente distribuir un certificado X.509
compartido entre las máquinas y configure las aplicaciones hospedadas para usar el certificado para el
cifrado de claves en reposo:

public void ConfigureServices(IServiceCollection services)


{
services.AddDataProtection()
.ProtectKeysWithCertificate("3BCE558E2AD3E0E34A7743EAB5AEA2A9BD2575A0");
}

Debido a limitaciones de .NET Framework, se admiten solo los certificados con claves privadas de CAPI. Ver
el contenido siguiente para buscar posibles soluciones a estas limitaciones.

DPAPI de Windows-NG
Este mecanismo solo está disponible en Windows 8/Windows Server 2012 o posterior.
A partir de Windows 8, sistema operativo Windows es compatible con DPAPI-NG (también denominado
CNG DPAPI). Para obtener más información, consulte sobre DPAPI de CNG.
La entidad de seguridad se codifica como una regla de descriptor de protección. En el ejemplo siguiente que
llama a ProtectKeysWithDpapiNG, solo el usuario de dominio con el SID especificado puede descifrar el
conjunto de claves:

public void ConfigureServices(IServiceCollection services)


{
// Uses the descriptor rule "SID=S-1-5-21-..."
services.AddDataProtection()
.ProtectKeysWithDpapiNG("SID=S-1-5-21-...",
flags: DpapiNGProtectionDescriptorFlags.None);
}

También hay una sobrecarga sin parámetros de ProtectKeysWithDpapiNG . Use este método de conveniencia
para especificar la regla "SID = {CURRENT_ACCOUNT_SID }", donde CURRENT_ACCOUNT_SID es el SID
de la cuenta de usuario de Windows actual:

public void ConfigureServices(IServiceCollection services)


{
// Use the descriptor rule "SID={current account SID}"
services.AddDataProtection()
.ProtectKeysWithDpapiNG();
}

En este escenario, el controlador de dominio de AD es responsable de distribuir las claves de cifrado


utilizadas por las operaciones de DPAPI NG. El usuario de destino pueda descifrar la carga cifrada desde
cualquier equipo unido al dominio (siempre que el proceso se ejecuta bajo su identidad).
Basada en certificados de cifrado con DPAPI de Windows-NG
Si la aplicación se ejecuta en Windows 8.1 o Windows Server 2012 R2 o versiones posteriores, puede usar
Windows DPAPI-NG para realizar el cifrado basada en certificados. Utilice la cadena de descriptor de la regla
"certificado = HashId:THUMBPRINT", donde huella digital es la huella digital con codificación hexadecimal
SHA1 del certificado:

public void ConfigureServices(IServiceCollection services)


{
services.AddDataProtection()
.ProtectKeysWithDpapiNG("CERTIFICATE=HashId:3BCE558E2...B5AEA2A9BD2575A0",
flags: DpapiNGProtectionDescriptorFlags.None);
}

Cualquier aplicación que señala a este repositorio debe estar ejecutándose en Windows 8.1 o Windows
Server 2012 R2 o posterior para descifrar las claves.

Cifrado de claves personalizado


Si los mecanismos en el equipo no son adecuados, el desarrollador puede especificar su propio mecanismo
de cifrado de claves al proporcionar una personalizada IXmlEncryptor.
Inmutabilidad de claves y valores de clave en
ASP.NET Core
25/07/2018 • 2 minutes to read • Edit Online

Una vez que un objeto se conserva en el almacén de respaldo, su representación es fija para siempre. Se pueden
agregar nuevos datos en el almacén de respaldo, 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, una vez que se escribe una clave en el almacén de respaldo, es
inmutable. Su fecha de creación, activación y expiración nunca se puede cambiar, aunque pueden revocar
utilizando IKeyManager . Además, su información algorítmico subyacente, material de claves maestra y el cifrado
en las propiedades de rest también son inmutables.
Si el desarrollador cambia cualquier configuración que afecta a la persistencia de clave, dichos cambios no entran
en vigor hasta la próxima vez que se genera 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ático generación
comportamiento. La configuración que afecta la persistencia de clave es los siguientes:
Duración de la clave predeterminada
El cifrado de claves en el mecanismo de rest
La información algorítmica contenida dentro de la clave
Si necesita esta configuración se activará anteriores a la siguiente clave 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 que tocar el repositorio deben especificar la misma configuración con el IDataProtectionBuilder
métodos de extensión. En caso contrario, las propiedades de la clave almacenada será dependientes de la aplicación
específica que invoca las rutinas de generación de claves.
Formato de almacenamiento de claves en ASP.NET
Core
25/07/2018 • 5 minutes to read • Edit Online

Los objetos se almacenan en reposo en la representación XML. El directorio predeterminado para el


almacenamiento de claves es % LOCALAPPDATA%\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. Estos archivos contienen 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 Id. de clave. Este valor se trata como autoritativo; el nombre de archivo es simplemente un nicety legible.
La versión de la <clave > elemento, que actualmente se fija en 1.
Fechas de creación, activación y expiración de la clave.
Un <descriptor > elemento, que contiene información sobre la implementación de cifrado autenticado
dentro de esta clave.
En el ejemplo anterior, el identificador de la clave es {80732141-ec8f-4b80-af9c-c4d2d1ff8901}, que se creó o
activado 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 una crítica molesta en
cómo las API funcionan y es inofensivas en la práctica).

El <descriptor > elemento


El exterior <descriptor > elemento contiene un atributo deserializerType, que es el nombre completo de
ensamblado de un tipo que implementa IAuthenticatedEncryptorDescriptorDeserializer. Este tipo es responsable
de leer interno <descriptor > elemento y para analizar la información contenida en.
El formato determinado 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, sin embargo, este elemento contendrá información algorítmico (nombres, tipos, OID, o similar) y
material de clave secreta. En el ejemplo anterior, el descriptor especifica que ajusta esta clave de cifrado de AES -
256-CBC + HMACSHA256 validación.

El <encryptedSecret > elemento


Un <encryptedSecret> puede encontrarse el elemento que contiene el formulario cifrado de la clave secreta si
está habilitado el cifrado de secretos en reposo. El atributo decryptorType es el nombre completo de ensamblado
de un tipo que implementa IXmlDecryptor. Este tipo es responsable de leer interno <encryptedKey> elemento y
lo descifra para recuperar el texto sin formato original.
Igual que con <descriptor >, el formato determinado de la elemento depende del 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 una sola
<revocación > elemento.
Para las revocaciones de las claves individuales, el contenido del archivo será como sigue.

<?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 antes de 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 > nunca se lee el elemento por el sistema. Es simplemente un lugar conveniente para almacenar una
razón legible de la revocación.
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 ASP.NET Core
21/09/2018 • 12 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.
Ninguna API puede evitar que a un cliente envía información confidencial en la primera solicitud.

WARNING
Hacer no usar RequireHttpsAttribute en las API Web que recibe información confidencial. RequireHttpsAttribute usa
códigos de estado HTTP para redirigir los 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:
No escucha en HTTP.
Cierre la conexión con el código de estado 400 (solicitud incorrecta) y no atender la solicitud.

Requerir HTTPS
Se recomienda toda la producción de ASP.NET Core llamada de aplicaciones web:
El Middleware de redireccionamiento de HTTPS (UseHttpsRedirection) para redirigir todas las solicitudes
HTTP a HTTPS.
UseHsts, protocolo de seguridad de transporte estricto HTTP (HSTS ).
UseHttpsRedirection
El código siguiente llama UseHttpsRedirection en el Startup clase:

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

El código resaltado anterior:


Usa el valor predeterminado HttpsRedirectionOptions.RedirectStatusCode ( Status307TemporaryRedirect ).
Usa el valor predeterminado HttpsRedirectionOptions.HttpsPort (null), a menos que se reemplaza por la
ASPNETCORE_HTTPS_PORT variable de entorno o IServerAddressesFeature.

WARNING
Un puerto debe estar disponible para el middleware redirigir a HTTPS. Si el puerto no está disponible, no se realizará la
redirección a HTTPS. El puerto HTTPS se puede especificar cualquiera de los siguientes ajustes:
HttpsRedirectionOptions.HttpsPort
El ASPNETCORE_HTTPS_PORT variable de entorno.
En el desarrollo, una dirección url HTTPS en launchsettings.json.
Una dirección url HTTPS configurada directamente en Kestrel o HttpSys.

Los siguientes resaltan las llamadas de código AddHttpsRedirection para configurar las opciones de
middleware:

public void ConfigureServices(IServiceCollection services)


{
services.AddMvc();

services.AddHsts(options =>
{
options.Preload = true;
options.IncludeSubDomains = true;
options.MaxAge = TimeSpan.FromDays(60);
options.ExcludedHosts.Add("example.com");
options.ExcludedHosts.Add("www.example.com");
});

services.AddHttpsRedirection(options =>
{
options.RedirectStatusCode = StatusCodes.Status307TemporaryRedirect;
options.HttpsPort = 5001;
});
}

Una llamada a AddHttpsRedirection sólo es necesario cambiar los valores de HttpsPort o RedirectStatusCode ;
El código resaltado anterior:
Conjuntos de HttpsRedirectionOptions.RedirectStatusCode a Status307TemporaryRedirect , que es el valor
predeterminado.
Establece el puerto HTTPS a 5001. El valor predeterminado es 443.
Los mecanismos siguientes establecen automáticamente el puerto:
El middleware puede detectar los puertos a través de IServerAddressesFeature cuando se aplican las
condiciones siguientes:
Kestrel o HTTP.sys se usa directamente con los puntos de conexión HTTPS (también se aplica a la
ejecución de la aplicación con el depurador de Visual Studio Code).
Solo un puerto HTTPS utilizado por la aplicación.
Se usa Visual Studio:
IIS Express tiene habilitadas para HTTPS.
launchSettings.json establece el sslPort para IIS Express.
NOTE
Cuando se ejecuta una aplicación detrás de un proxy inverso (por ejemplo, IIS, IIS Express), IServerAddressesFeature
no está disponible. El puerto debe configurarse manualmente. Cuando no se configura el puerto, no se redirigen las
solicitudes.

El puerto puede configurarse estableciendo el https_port de configuración de Host Web:


Clave: https_port
Tipo: cadena
Valor predeterminado: no se establece un valor predeterminado.
Establecer mediante: UseSetting
Variable de entorno: <PREFIX_>HTTPS_PORT (el prefijo es ASPNETCORE_ cuando se usa el Host de Web.)

WebHost.CreateDefaultBuilder(args)
.UseSetting("https_port", "8080")

NOTE
Se puede configurar el puerto 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 detecta indirectamente el puerto
HTTPS a través de IServerAddressesFeature .

Si no se establece ningún puerto:


No se redirigen las solicitudes.
El middleware registra la advertencia "No se pudo determinar el puerto https para la redirección."

NOTE
Una alternativa al uso de Middleware de redireccionamiento de HTTPS ( UseHttpsRedirection ) consiste en usar el
Middleware de reescritura de dirección URL ( AddRedirectToHttps ). AddRedirectToHttps También se 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 URL.
Al redirigir a HTTPS sin necesidad de reglas de redirección adicionales, se recomienda usar software intermedio de
redireccionamiento de HTTPS ( UseHttpsRedirection ) se describe en este tema.

El RequireHttpsAttribute se usa para requerir HTTPS. [RequireHttpsAttribute] puede decorar los


controladores o métodos, o se pueden aplicar globalmente. Para aplicar el atributo global, agregue el código
siguiente al ConfigureServices en Startup :

// Requires using Microsoft.AspNetCore.Mvc;


public void ConfigureServices(IServiceCollection services)
{
services.Configure<MvcOptions>(options =>
{
options.Filters.Add(new RequireHttpsAttribute());
});

El código resaltado anterior requiere que todas las solicitudes usan HTTPS ; por lo tanto, se omiten las
solicitudes HTTP. El código resaltado siguiente redirige todas las solicitudes HTTP a HTTPS:
// Requires using Microsoft.AspNetCore.Rewrite;
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();

var options = new RewriteOptions()


.AddRedirectToHttps();

app.UseRewriter(options);

Para obtener más información, consulte Middleware de reescritura de URL. El software intermedio 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
se 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 estricto HTTP (HSTS)


Por OWASP, seguridad de transporte estricto (HSTS ) de HTTP es una mejora de seguridad opcional que se
especifica mediante una aplicación web mediante el uso de un encabezado de respuesta. Cuando un
explorador que admita HSTS recibe este encabezado:
El explorador almacena la configuración para el dominio que impide el envío de cualquier comunicación a
través de HTTP. El explorador obliga a todas las comunicaciones a través de HTTPS.
El explorador impide que el usuario mediante certificados de confianza o no es válidos. El explorador
deshabilita avisos para que un usuario que confíe en temporalmente dicho certificado.
Porque se aplica HSTS por el cliente tiene algunas limitaciones:
El cliente debe admitir HSTS.
HSTS requiere al menos una solicitud HTTPS correcta para establecer la directiva HSTS.
La aplicación debe comprobar todas las solicitudes HTTP y redirigir o rechazar la solicitud HTTP.
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:

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();
}
UseHsts no se recomienda en el desarrollo porque la configuración de HSTS sea altamente almacenable en
caché por los exploradores. De forma predeterminada, UseHsts excluye la dirección de bucle invertido local.
Para entornos de producción implementar HTTPS por primera vez, establezca el valor HSTS inicial en un valor
pequeño. Establezca el valor de horas en no más de un solo día en caso de que necesite revertir la
infraestructura HTTPS a HTTP. Una vez que esté seguro en la sostenibilidad de la configuración de HTTPS,
aumente el valor de max-age HSTS; un valor frecuente es un año.
El código siguiente:

public void ConfigureServices(IServiceCollection services)


{
services.AddMvc();

services.AddHsts(options =>
{
options.Preload = true;
options.IncludeSubDomains = true;
options.MaxAge = TimeSpan.FromDays(60);
options.ExcludedHosts.Add("example.com");
options.ExcludedHosts.Add("www.example.com");
});

services.AddHttpsRedirection(options =>
{
options.RedirectStatusCode = StatusCodes.Status307TemporaryRedirect;
options.HttpsPort = 5001;
});
}

Establece el parámetro precarga del encabezado Strict-Transport-Security. Precarga no forma parte de la


especificación RFC HSTS, pero es compatible con los exploradores web para cargar previamente sitios
HSTS en una instalación nueva. Para más información, vea https://hstspreload.org/.
Permite includeSubDomain, que aplica la directiva HSTS para hospedar subdominios.
Establece explícitamente el parámetro de antigüedad máxima del encabezado Strict-Transport-Security en
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 para excluir.
UseHsts excluye los hosts de bucle invertido siguientes:
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.

El ejemplo anterior muestra cómo agregar hosts adicionales.

Desactivación de HTTPS en la creación del proyecto


Habilitan las plantillas de aplicación de ASP.NET Core web 2.1 o posterior (en Visual Studio o la línea de
comandos de dotnet) redirección HTTPS y HSTS. Para las implementaciones que no requieran HTTPS, puede
participar en HTTPS. Por ejemplo, algunos servicios de back-end donde HTTPS se está controlando
externamente en el perímetro, mediante HTTPS en cada nodo no es necesario.
Para participar en HTTPS:
Visual Studio
CLI de .NET Core
Desactive el configurar HTTPS casilla de verificación.

Cómo configurar un certificado de desarrollador para Docker


Consulte este problema de GitHub.

Información adicional
Compatibilidad con exploradores de OWASP HSTS
Soporte técnico del Reglamento General de
protección de datos (RGPD) de la UE en ASP.NET
Core
21/09/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 Reglamento General de
protección de datos (RGPD ) de la UE requisitos:
Las plantillas de proyecto incluyen puntos de extensión y auxiliar marcado que se puede reemplazar con la
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 información personal. Si un usuario no ha dado su consentimiento para la
recopilación de datos y la aplicación tiene CheckConsentNeeded establecido en true , las cookies no
esenciales no se envían al explorador.
Las cookies se pueden marcar como esenciales. Esencial cookies se envían al explorador incluso cuando el
usuario no ha dado su consentimiento y seguimiento está deshabilitado.
Las cookies de TempData y Session no funcionan cuando se deshabilita el seguimiento.
El administrar identidades página proporciona un vínculo para descargar y eliminar datos de usuario.
El aplicación de ejemplo permite probar la mayoría de los puntos de extensión de RGPD y las API agregadas
a las plantillas de ASP.NET Core 2.1. Consulte la Léame archivo para las pruebas de instrucciones.
Vea o descargue el código de ejemplo (cómo descargarlo)

Compatibilidad con GDPR de ASP.NET Core en el código de


plantilla generada
Las páginas de Razor y MVC los proyectos creados con las plantillas de proyecto incluyen la compatibilidad
de RGPD siguiente:
CookiePolicyOptions y UseCookiePolicy se establecen en Startup .
El _CookieConsentPartial.cshtml vista parcial.
El Pages/Privacy.cshtml página o Views/Home/Privacy.cshtml vista proporciona una página para detallar
política de privacidad de su sitio. El _CookieConsentPartial.cshtml archivo genera un vínculo a la página
de privacidad.
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 Startup.ConfigureServices :
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>();

// If the app uses session state, call AddSession.


// services.AddSession();

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

// If the app uses session state, call Session Middleware after Cookie
// Policy Middleware and before MVC Middleware.
// app.UseSession();

app.UseMvc();
}
}

UseCookiePolicy se denomina en Startup.Configure :


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

// If the app uses session state, call AddSession.


// services.AddSession();

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

// If the app uses session state, call Session Middleware after Cookie
// Policy Middleware and before MVC Middleware.
// app.UseSession();

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 está configurada para requerir
consentimiento, el usuario debe dar su consentimiento antes de que pueden realizar el seguimiento de las
cookies. Si se requiere el consentimiento, el panel de consentimiento de la cookie se fija en la parte
superior de la barra de navegación creada por el _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 la página de privacidad o vista donde puede detallan la directiva de privacidad
de su sitio.

Cookies esenciales
Si no tiene concedido el consentimiento, solo las cookies de marcado esencial 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 son


esenciales
El proveedor Tempdata cookie no es esencial. Si se deshabilita el seguimiento, el proveedor Tempdata no
funcionará. Para habilitar el proveedor Tempdata cuando se deshabilita el seguimiento, marcar la cookie de
TempData como esencial en Startup.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 las cookies no son esenciales. Estado de sesión no funcionará 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 el impacto solamente los datos de identidad predeterminada. Las aplicaciones que
crean datos de usuario personalizada deben ampliarse para eliminación y descarga los datos de usuario
personalizada. Para obtener más información, consulte Add, descargar y eliminar datos de usuario
personalizada para identidad.
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 otros trabajos para el software que tiene acceso a los datos.
Es la opción más sencilla y segura.
Permite administrar las claves y cifrado de la base de datos.
Por ejemplo:
Microsoft SQL y SQL de Azure proporcionan cifrado de datos transparente (TDE ).
SQL Azure cifra la base de datos de forma predeterminada
Azure Blobs, archivos, Table y Queue Storage se cifran de forma predeterminada.
Para las bases de datos que no proporcionan cifrado integrado en reposo, puede usar el cifrado de disco para
proporcionar la misma protección. Por ejemplo:
BitLocker para Windows Server
Linux:
eCryptfs
EncFS.

Recursos adicionales
Microsoft.com/GDPR
Almacenamiento seguro de secretos de
aplicación en el desarrollo en ASP.NET Core
25/09/2018 • 17 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 ASP.NET Core. Nunca almacene contraseñas u otros datos
confidenciales en el código fuente. Secretos de producción no deben usarse para el desarrollo o
prueba. Puede almacenar y proteger sus secretos de producción y pruebas de Azure con el
proveedor de configuración de Azure Key Vault.

Variables de entorno
Las variables de entorno se usan para evitar el almacenamiento de secretos de aplicación en el
código o en archivos de configuración local. Las variables de entorno invalidan los valores de
configuración para todos los orígenes de configuración especificada anteriormente.
Configurar la lectura de valores de variables de entorno mediante una llamada a
AddEnvironmentVariables en el Startup constructor:

public Startup(IHostingEnvironment env)


{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json",
optional: false,
reloadOnChange: true)
.AddEnvironmentVariables();

if (env.IsDevelopment())
{
builder.AddUserSecrets<Startup>();
}

Configuration = builder.Build();
}

Considere la posibilidad de una aplicación web de ASP.NET Core en el 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 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 se
puede invalidar el valor de clave con el valor de la variable de entorno. La variable de entorno
puede almacenar la cadena de conexión completa con 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, las variables de entorno pueden tener acceso por partes de confianza.
Medidas adicionales para evitar la divulgación de secretos de usuario pueden ser necesarias.

Administrador de secretos
La herramienta Secret Manager almacena datos confidenciales durante el desarrollo de un
proyecto de ASP.NET Core. En este contexto, un fragmento de datos confidenciales es un secreto
de la aplicación. Los secretos de aplicación se almacenan en una ubicación independiente desde el
árbol del proyecto. Los secretos de aplicación se asociado con un proyecto específico o se
comparten entre varios proyectos. No se comprueban los secretos de aplicación en control de
código fuente.

WARNING
La herramienta Secret Manager no cifra los secretos almacenados y no debe tratarse como un almacén de
confianza. Es solo con fines de desarrollo. Las claves y valores se almacenan en un archivo de configuración
de JSON en el directorio del perfil de usuario.

Cómo funciona la herramienta Secret Manager


La herramienta Secret Manager 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 perfil de
usuario protegidos por el sistema en el equipo local:
Windows
macOS
Linux
Ruta de acceso de archivo del sistema:
%APPDATA%\Microsoft\UserSecrets\<user_secrets_id>\secrets.json

En la anterior, 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 guardados con la
herramienta Secret Manager. Pueden cambiar estos detalles de implementación. Por ejemplo, los
valores de secreto no se cifran, pero podrían ser en el futuro.

Instalar la herramienta Secret Manager


La herramienta Secret Manager está empaquetado con la CLI de .NET Core SDK de .NET Core
2.1.300 o posterior. Para las versiones de SDK de .NET Core 2.1.300 antes de la instalación de
herramientas es necesaria.

TIP
Ejecute dotnet --version desde un shell de comandos para ver el número de versión del SDK de .NET
Core instalado.
Se mostrará una advertencia si se usa el SDK de .NET Core 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:

<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp1.1</TargetFramework>
<UserSecretsId>1242d6d6-9df3-4031-b031-d9b27d13c25a</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore"
Version="1.1.6" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets"
Version="1.1.2" />
<PackageReference Include="System.Data.SqlClient"
Version="4.5.0" />
</ItemGroup>
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.Extensions.SecretManager.Tools"
Version="1.0.1" />
</ItemGroup>
</Project>

Ejecute el siguiente comando en un shell de comandos para validar la instalación de herramienta:

dotnet user-secrets -h

La herramienta Secret Manager muestra el uso de ejemplo, las opciones y ayuda del 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 va a ejecutar las herramientas que se definen en el
.csproj del archivo DotNetCliToolReference elementos.
Establezca un secreto
La herramienta Secret Manager opera en valores de configuración de específicas del proyecto
almacenados en su perfil de usuario. Para usar secretos de usuario, defina un 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
.

<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<UserSecretsId>79a3edd0-2092-40a2-a04d-dcb46d5ca9ed</UserSecretsId>
</PropertyGroup>

<PropertyGroup>
<TargetFramework>netcoreapp1.1</TargetFramework>
<UserSecretsId>1242d6d6-9df3-4031-b031-d9b27d13c25a</UserSecretsId>
</PropertyGroup>

TIP
En Visual Studio, haga clic en el proyecto en el Explorador de soluciones y seleccione administrar secretos
de usuario en el menú contextual. Este movimiento se agrega un UserSecretsId elemento, 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:

{
"Movies": {
"ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=Movie-
1;Trusted_Connection=True;MultipleActiveResultSets=true",
"ServiceApiKey": "12345"
}
}

Se aplana la estructura JSON después de las modificaciones a través de dotnet user-secrets remove o
dotnet user-secrets set . Por ejemplo, ejecutar
dotnet user-secrets remove "Movies:ConnectionString" contrae el Movies literal de objeto. El
archivo modificado se parece a lo siguiente:

{
"Movies:ServiceApiKey": "12345"
}

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 una
ServiceApiKey propiedad.

La herramienta Secret Manager puede usarse también de otros directorios. Use la --project
opción para proporcionar la ruta de acceso de archivo del sistema en el que el .csproj archivo existe.
Por ejemplo:
dotnet user-secrets set "Movies:ServiceApiKey" "12345" --project "C:\apps\WebApp1\src\WebApp1"

Establezca varios secretos


Un lote de los secretos se puede establecer mediante la canalización de JSON para el 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 Secret Manager. Si
el proyecto tiene como destino .NET Framework, instalar el
Microsoft.Extensions.Configuration.UserSecrets paquete NuGet.
En ASP.NET Core 2.0 o posterior, el origen de configuración de los secretos de usuario se agrega
automáticamente en modo de desarrollo cuando el proyecto llama a CreateDefaultBuilder para
inicializar una nueva instancia del host con valores predeterminados preconfigurados.
CreateDefaultBuilder las llamadas AddUserSecrets cuando el EnvironmentName es desarrollo:

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>


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

Cuando CreateDefaultBuilder no llama durante la construcción de host, agregar el origen de


configuración de los secretos de usuario con una llamada a AddUserSecrets en el Startup
constructor:

public Startup(IHostingEnvironment env)


{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json",
optional: false,
reloadOnChange: true)
.AddEnvironmentVariables();

if (env.IsDevelopment())
{
builder.AddUserSecrets<Startup>();
}

Configuration = builder.Build();
}

El API de configuración de ASP.NET Core proporciona acceso a los secretos de Secret Manager.
Instalar el Microsoft.Extensions.Configuration.UserSecrets paquete NuGet.
Agregar el origen de configuración de los secretos de usuario con una llamada a AddUserSecrets
en el Startup constructor:

public Startup(IHostingEnvironment env)


{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json",
optional: false,
reloadOnChange: true)
.AddEnvironmentVariables();

if (env.IsDevelopment())
{
builder.AddUserSecrets<Startup>();
}

Configuration = builder.Build();
}

Se pueden recuperar secretos del usuario a través de la Configuration API:

public class Startup


{
private string _moviesApiKey = null;

public Startup(IConfiguration configuration)


{
Configuration = configuration;
}

public IConfiguration Configuration { get; }

public void ConfigureServices(IServiceCollection services)


{
_moviesApiKey = Configuration["Movies:ServiceApiKey"];
}

public void Configure(IApplicationBuilder app)


{
var result = string.IsNullOrEmpty(_moviesApiKey) ? "Null" : "Not Null";
app.Run(async (context) =>
{
await context.Response.WriteAsync($"Secret is {result}");
});
}
}
public class Startup
{
private string _moviesApiKey = null;

public Startup(IHostingEnvironment env)


{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json",
optional: false,
reloadOnChange: true)
.AddEnvironmentVariables();

if (env.IsDevelopment())
{
builder.AddUserSecrets<Startup>();
}

Configuration = builder.Build();
}

public IConfigurationRoot Configuration { get; }

public void ConfigureServices(IServiceCollection services)


{
_moviesApiKey = Configuration["Movies:ServiceApiKey"];
}

public void Configure(IApplicationBuilder app)


{
var result = string.IsNullOrEmpty(_moviesApiKey) ? "Null" : "Not Null";
app.Run(async (context) =>
{
await context.Response.WriteAsync($"Secret is {result}");
});
}
}

Los secretos se asignan a un objeto POCO


Asignación de un literal de objeto completo a un objeto POCO (una clase .NET simple con
propiedades) es útil para la agregación de las propiedades relacionadas.
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"
}
}

Para asignar los secretos anteriores a un objeto POCO, utilice el Configuration API el enlace del
gráfico de objetos característica. El código siguiente se enlaza a una personalizada MovieSettings
POCO y tiene acceso a la ServiceApiKey valor de propiedad:

var moviesConfig = Configuration.GetSection("Movies")


.Get<MovieSettings>();
_moviesApiKey = moviesConfig.ServiceApiKey;
var moviesConfig = new MovieSettings();
Configuration.GetSection("Movies").Bind(moviesConfig);
_moviesApiKey = moviesConfig.ServiceApiKey;

El Movies:ConnectionString y Movies:ServiceApiKey secretos se asignan a las propiedades


correspondientes en MovieSettings :

public class MovieSettings


{
public string ConnectionString { get; set; }

public string ServiceApiKey { get; set; }


}

Cadena de reemplazo con secretos


No es seguro almacenar contraseñas como texto sin formato. Por ejemplo, una cadena de conexión
de base de datos se almacena en appsettings.json 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 es almacenar la contraseña como un secreto. Por ejemplo:

dotnet user-secrets set "DbPassword" "pass123"

Quitar el Password par clave-valor de la cadena de conexión en appsettings.json. 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:
public class Startup
{
private string _connection = null;

public Startup(IConfiguration configuration)


{
Configuration = configuration;
}

public IConfiguration Configuration { get; }

public void ConfigureServices(IServiceCollection services)


{
var builder = new SqlConnectionStringBuilder(
Configuration.GetConnectionString("Movies"));
builder.Password = Configuration["DbPassword"];
_connection = builder.ConnectionString;
}

public void Configure(IApplicationBuilder app)


{
app.Run(async (context) =>
{
await context.Response.WriteAsync($"DB Connection: {_connection}");
});
}
}
public class Startup
{
private string _connection = null;

public Startup(IHostingEnvironment env)


{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json",
optional: false,
reloadOnChange: true)
.AddEnvironmentVariables();

if (env.IsDevelopment())
{
builder.AddUserSecrets<Startup>();
}

Configuration = builder.Build();
}

public IConfigurationRoot Configuration { get; }

public void ConfigureServices(IServiceCollection services)


{
var builder = new SqlConnectionStringBuilder(
Configuration.GetConnectionString("Movies"));
builder.Password = Configuration["DbPassword"];
_connection = builder.ConnectionString;
}

public void Configure(IApplicationBuilder app)


{
app.Run(async (context) =>
{
await context.Response.WriteAsync($"DB Connection: {_connection}");
});
}
}

Enumera 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

Aparece el siguiente resultado:


Movies:ConnectionString = Server=(localdb)\mssqllocaldb;Database=Movie-
1;Trusted_Connection=True;MultipleActiveResultSets=true
Movies:ServiceApiKey = 12345

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 con el
MoviesConnectionString clave:

{
"Movies": {
"ServiceApiKey": "12345"
}
}

Ejecutando dotnet user-secrets list muestra el mensaje siguiente:

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 para la aplicación de la secrets.json archivo:
{}

Ejecutando dotnet user-secrets list muestra el mensaje siguiente:

No secrets configured for this application.

Recursos adicionales
Configuración en ASP.NET Core
Proveedor de configuración de Azure Key Vault en ASP.NET Core
Proveedor de configuración de Azure Key Vault en
ASP.NET Core
27/08/2018 • 16 minutes to read • Edit Online

Por Halter y Andrew Stanton-Nurse


Este documento explica cómo usar el Microsoft Azure Key Vault proveedor de configuración para cargar los
valores de configuración de aplicación de secretos de Azure Key Vault. Azure Key Vault es un servicio basado
en la nube que le ayuda a proteger claves criptográficas y secretos usados por aplicaciones y servicios.
Escenarios comunes incluyen control de acceso a datos confidenciales de la configuración y cumple el
requisito de FIPS 140-2 nivel 2 validado módulos de seguridad de Hardware (HSM ) al almacenar datos de
configuración. Esta característica está disponible para las aplicaciones que tienen como destino ASP.NET
Core 1.1 o posterior.
Vea o descargue el código de ejemplo (cómo descargarlo)

Package
Para usar el proveedor, agregue una referencia a la Microsoft.Extensions.Configuration.AzureKeyVault
paquete.

Configuración de la aplicación
Puede explorar el proveedor con el aplicaciones de ejemplo. Después de establecer un almacén de claves y
cree los secretos en el almacén, las aplicaciones de ejemplo carga los valores de secreto en sus
configuraciones de forma segura y mostrarlos en las páginas Web.
El proveedor se agrega a la configuración de la aplicación con el AddAzureKeyVault extensión. En las
aplicaciones de ejemplo, la extensión usa tres valores de configuración cargados desde el appsettings.json
archivo.

CONFIGURACIÓN DE LA APLICACIÓN DESCRIPCIÓN EJEMPLO

Vault Nombre del almacén de claves de contosovault


Azure

ClientId Identificador de aplicación de Azure 627e911e-43cc-61d4-992e-


Active Directory 12db9c81b413

ClientSecret Clave de la aplicación de Azure Active g58K3dtg59o1Pa+e59v2Tx829w6VxT


Directory B2yv9sv/101di=
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((context, config) =>
{
var builtConfig = config.Build();

config.AddAzureKeyVault(
$"https://{builtConfig["Vault"]}.vault.azure.net/",
builtConfig["ClientId"],
builtConfig["ClientSecret"]);
})
.UseStartup<Startup>();

Creación de los secretos del almacén de claves y carga 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 Azure Key Vault.
Agregue secretos al almacén de claves con el módulo de PowerShell AzureRM Key Vault
disponibles desde el Galería de PowerShell, el API de REST de Azure Key Vault, o el Portal 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 usar 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 secretos de Azure Key
Vault se limitan a caracteres alfanuméricos y guiones.
Usan valores jerárquicos (las secciones de configuración) -- (dos guiones) como separador
en el ejemplo. Dos puntos, que normalmente se utilizan para delimitar una sección de una
subclave en configuración de ASP.NET Core, no se permiten en los nombres de secretos.
Por lo tanto, son 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 de la primera
es un nombre simple y el valor y el secreto de la segunda crea un valor secreto con una
sección y la subclave en el nombre del secreto:
SecretName : secret_value_1
Section--SecretName : secret_value_2
Registrar la aplicación de ejemplo con Azure Active Directory.
Autorizar a la aplicación para acceder al almacén de claves. Cuando se usa el
Set-AzureRmKeyVaultAccessPolicy proporcionan el cmdlet de PowerShell para autorizar a la
aplicación para tener acceso al almacén de claves List y Get acceso a los secretos con
-PermissionsToSecrets list,get .
2. Actualización de la aplicación appsettings.json 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 del secreto.
Valores que no son jerárquicos: el valor de SecretName se obtiene con config["SecretName"] .
Los valores jerárquicos (secciones): Use : notación (dos puntos) o el GetSection método de
extensión. Para obtener el valor de configuración, use cualquiera de estos enfoques:
config["Section:SecretName"]
config.GetSection("Section")["SecretName"]

Al ejecutar la aplicación, una página Web muestra los valores de secreto cargados:

Los secretos del almacén de claves con prefijo de crear y cargar los
valores de configuración (clave de nombre de prefijo ejemplo)
AddAzureKeyVault También proporciona una sobrecarga que acepta una implementación de
IKeyVaultSecretManager , que le permite controlar los secretos del almacén clave se convierten en las claves
de configuración. Por ejemplo, puede implementar la interfaz para cargar los valores de secreto según un
valor de prefijo que se proporcione al iniciar la aplicación. Esto 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 para colocar los secretos de varias aplicaciones en el mismo
almacén de claves o colocar los secretos del entorno (por ejemplo, desarrollo frente a producción secretos) en la misma
almacén. Se recomienda que diferentes aplicaciones y entornos de desarrollo y producción usan almacenes de 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 (no se
permiten los períodos en los nombres de secretos del almacén de claves) que representa un secreto de la
aplicación para la versión 5.0.0.0 de la aplicación. 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,
la versión cuando se cargue el secreto. La 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();

config.AddAzureKeyVault(
$"https://{builtConfig["Vault"]}.vault.azure.net/",
builtConfig["ClientId"],
builtConfig["ClientSecret"],
new PrefixKeyVaultSecretManager(versionPrefix));
})
.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 se llama al método mediante un algoritmo de proveedor que recorre en iteración los secretos del
almacén para buscar las que tienen el prefijo de la versión. Cuando se encuentra un prefijo de la versión con
Load , el algoritmo utiliza el GetKey método para devolver el nombre de la 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
la carga 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 la cadena de 5000-AppSecret coincide.
3. La versión, 5000 (con el guión), se quitan el nombre de clave dejando AppSecret carga con el valor del
secreto en la configuración de la aplicación.

NOTE
También puede proporcionar su propia 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 Azure Key Vault.
Agregue secretos al almacén de claves con el módulo de PowerShell AzureRM Key Vault
disponibles desde el Galería de PowerShell, el API de REST de Azure Key Vault, o el Portal 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 usar 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 (las 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 a la aplicación para acceder al almacén de claves. Cuando se usa el
Set-AzureRmKeyVaultAccessPolicy proporcionan el cmdlet de PowerShell para autorizar a la
aplicación para tener acceso al almacén de claves List y Get acceso a los secretos con
-PermissionsToSecrets list,get .

2. Actualización de la aplicación appsettings.json 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 agregó el proveedor de
configuración de Azure Key Vault. 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. Esta vez, devuelve el valor del secreto 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 Secret Manager para mantener la ClientSecret fuera de un árbol de origen del proyecto.
Con el Administrador de secretos, asocie los secretos de aplicación con un proyecto específico y compartirlos
entre varios proyectos.
Al desarrollar una aplicación de .NET Framework en un entorno que admite los certificados, puede
autenticarse en Azure Key Vault 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 ( _env en el ejemplo
siguiente:
var builtConfig = config.Build();

var store = new X509Store(StoreLocation.CurrentUser);


store.Open(OpenFlags.ReadOnly);
var cert = store.Certificates
.Find(X509FindType.FindByThumbprint,
config["CertificateThumbprint"], false);

config.AddAzureKeyVault(
builtConfig["Vault"],
builtConfig["ClientId"],
cert.OfType<X509Certificate2>().Single(),
new EnvironmentSecretManager(context.HostingEnvironment.ApplicationName));

store.Close();

Volver a cargar los secretos


Los secretos se almacenan en caché hasta que IConfigurationRoot.Reload() se llama. Ha expirado,
deshabilitado, y actualizado los secretos del almacén de claves no se respeten la aplicación hasta que Reload
se ejecuta.

Configuration.Reload();

Secretos deshabilitados y expirados


Generar secretos expirados y deshabilitados una KeyVaultClientException . Para evitar que la aplicación
genere, reemplace la aplicación o actualizar el secreto deshabilitado o expirado.

Solucionar 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 de registro de ASP.NET Core. Configuración de carga evitará que las condiciones
siguientes:
La aplicación no está configurada correctamente en Azure Active Directory.
El almacén de claves no existe en Azure Key Vault.
La aplicación no está autorizada para acceder al almacén de claves.
La directiva de acceso no incluye Get y List permisos.
En el almacén de claves, los datos de configuración (par nombre-valor) denominados 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 Azure AD ( ClientSecret ).
La clave de Azure AD ( 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 en ASP.NET Core
Microsoft Azure: Almacén de claves
Microsoft Azure: Documentación del almacén de claves
Para generar y transferir protegidas con HSM de claves para Azure Key Vault
Clase KeyVaultClient
Evitar Cross-Site falsificación de solicitud
entre (XSRF/CSRF) attacks en ASP.NET Core
18/07/2018 • 26 minutes to read • Edit Online

Por Steve Smith, Fiyaz Hasan, y Rick Anderson


Falsificación de solicitud entre sitios (también conocida como XSRF o CSRF, pronunciado vea surf)
es un ataque contra aplicaciones hospedadas en web mediante el cual una aplicación web
malintencionado puede influir en la interacción entre un explorador cliente y una aplicación web
que confía en que 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 se denomina un ataque de un solo clic o sesión
volando porque el ataque aprovecha las ventajas de sesión del autenticado 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 que
recibe 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 publicaciones en el sitio vulnerable, no en 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 se puede realizar un
usuario autenticado.
Además de la situación donde el usuario selecciona el botón para enviar el formulario, el sitio
malintencionado podría:
Ejecutar un script que envía el formulario de forma automática.
Enviar el envío del formulario como una solicitud AJAX.
Ocultar el formulario mediante CSS.
Estos escenarios alternativos no requieren ninguna otra acción o la entrada del usuario que no sea
inicialmente visitando el sitio malintencionado.
Uso de HTTPS no impide que un ataque CSRF. El sitio Web malintencionado puede enviar un
https://www.good-banking-site.com/ solicitar tan fácilmente como puede enviar una solicitud poco
segura.
Algunos ataques van dirigidos a puntos de conexión que respondan a solicitudes GET, en cuyo
caso se puede usar 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 en las solicitudes GET, donde se modifican las variables o los recursos, son
vulnerables a ataques malintencionados. Las solicitudes GET que cambian 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 utilizan 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.
Los 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 a la aplicación dentro
del explorador.
Sin embargo, los ataques CSRF no están limitados a aprovechar las cookies. Por ejemplo, la
autenticación básica e implícita también son vulnerables. Después de que un usuario inicia sesión
con la 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 frente a vulnerabilidades CSRF tomando precauciones:
Inicie sesión fuera de 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.

Aspectos básicos de autenticación


Autenticación basada en cookies es una forma popular de autenticación. Los sistemas de
autenticación basada en token están creciendo en popularidad, especialmente para aplicaciones de
página única (SPA).
Autenticación basada en cookies
Cuando un usuario se autentica con su nombre de usuario y contraseña, se 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 cookies. El middleware
serializa un principal de usuario en una cookie cifrada. Valida la cookie en solicitudes posteriores, el
middleware, vuelve a crear la entidad de seguridad y asigna la entidad de seguridad para el usuario
propiedad de HttpContext.
Autenticación basada en token
Cuando un usuario se autentica, se le emite un token (no un token antifalsificación). El token
contiene información de usuario en forma de notificaciones o un 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 solicitudes posteriores, almacenar el token en el
almacenamiento local del explorador. No se preocupe sobre una vulnerabilidad CSRF, si el token
se almacena en el almacenamiento local del explorador. CSRF constituye un problema cuando el
token se almacena en una cookie.
Varias aplicaciones hospedadas en un dominio
Entornos de hospedaje compartidos son vulnerables al 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 la otra las cookies (las
directivas de mismo origen que rigen las solicitudes AJAX no se aplican necesariamente a las
cookies HTTP ).
Al no compartir dominios, se pueden evitar ataques que aprovechan las cookies de confianza entre
las aplicaciones hospedadas en el mismo dominio. 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 antifalsificación


WARNING
ASP.NET Core se implementa utilizando antifalsificación 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. Consulte configurar
la protección de datos para obtener más información.

En ASP.NET Core 2.0 o posterior, el FormTagHelper inserta tokens antifalsificación en elementos


de formulario HTML. El siguiente marcado en un archivo Razor genera automáticamente los
tokens antifalsificación:

<form method="post">
...
</form>

De forma similar, IHtmlHelper.BeginForm genera tokens antifalsificación de forma


predeterminada, si el método del formulario no es GET.
La generación automática de tokens antifalsificación 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 action está vacío ( action="" ).
No se proporciona el atributo de acción ( <form method="post"> ).

Se puede deshabilitar la generación automática de tokens antifalsificación para los elementos de


formulario HTML:
Deshabilite explícitamente tokens antifalsificación con la asp-antiforgery atributo:

<form method="post" asp-antiforgery="false">


...
</form>

El elemento de formulario es elegido horizontal de aplicaciones auxiliares de etiquetas


mediante el uso de la aplicación auxiliar de etiquetas ! participar símbolo:

<!form method="post">
...
</!form>

Quitar el FormTagHelper desde la vista. El FormTagHelper puede quitarse de una vista


mediante la adición de la directiva siguiente a la vista de Razor:

@removeTagHelper Microsoft.AspNetCore.Mvc.TagHelpers.FormTagHelper,
Microsoft.AspNetCore.Mvc.TagHelpers

NOTE
Las páginas de Razor están protegidos automáticamente frente 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 de CSRF es usar el 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 de 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 e imprevisible. El token puede usarse también para garantizar la secuencia
correcta de una serie de solicitudes (por ejemplo, lo que garantiza la secuencia de solicitud de:
página 1 – página 2 – página 3). Todos los formularios en las plantillas de ASP.NET Core MVC y
páginas de Razor de generan tokens antifalsificación. El siguiente par de ejemplos de vista genera
tokens antifalsificación:

<form asp-controller="Manage" asp-action="ChangePassword" method="post">


...
</form>

@using (Html.BeginForm("ChangePassword", "Manage"))


{
...
}

Agregar explícitamente un token de antifalsificación en un <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 antifalsificación:
ValidateAntiForgeryToken
AutoValidateAntiforgeryToken
IgnoreAntiforgeryToken

Opciones de antifalsificación
Personalizar opciones antifalsificación 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 antifalsificación.

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 se establece, el sistema


genera un nombre único a partir del
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 de formulario oculto utilizado


por el sistema antifalsificación para representar los
tokens antifalsificación en las vistas.
OPCIÓN DESCRIPCIÓN

HeaderName El nombre del encabezado usado por el sistema


antifalsificación. Si null , el sistema considera que
sólo los datos.

RequireSsl Especifica si el sistema antifalsificación requiere SSL. Si


true , producirá un error de las solicitudes que no
es 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 antifalsificación con IAntiforgery


IAntiforgery proporciona la API para configurar características antifalsificación. IAntiforgery se
puede solicitar en el Configure método de la Startup clase. En el ejemplo siguiente se usa el
software intermedio de la página principal de la aplicación para generar un token de
antifalsificación y enviarlo en la respuesta como una cookie (mediante la convención de
nomenclatura de forma 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 antifalsificación


ValidateAntiForgeryToken es un filtro de acción que se puede aplicar a una acción individual, un
controlador, o globalmente. Las solicitudes realizadas a las acciones que se haya aplicado este filtro
se bloquean a menos que la solicitud incluye un token antifalsificación 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 decora, incluidas las solicitudes HTTP GET. Si el ValidateAntiForgeryToken atributo se aplica
en todos los controladores de la aplicación, aunque puede reemplazarse con el
IgnoreAntiforgeryToken atributo.

NOTE
ASP.NET Core no admite agregar tokens antifalsificación automáticamente a las solicitudes GET.

Validar automáticamente los tokens antifalsificación para solo los métodos HTTP no seguros
Aplicaciones de ASP.NET Core no generan tokens antifalsificación para los métodos HTTP
seguros (GET, HEAD, opciones y seguimiento). En lugar de aplicar ampliamente el
ValidateAntiForgeryToken atributo y, a continuación, invalidar con IgnoreAntiforgeryToken
atributos, el AutoValidateAntiforgeryToken se puede usar el atributo. Este atributo funciona de
forma idéntica a la ValidateAntiForgeryToken atributo, salvo que no requiere tokens para las
solicitudes realizadas mediante los siguientes métodos HTTP:
GET
HEAD
OPCIONES
TRACE
Se recomienda usar AutoValidateAntiforgeryToken ampliamente para escenarios que no son API.
Esto garantiza que las acciones POST están protegidas de forma predeterminada. La alternativa
consiste en pasar por alto los tokens antifalsificación de forma predeterminada, a menos que
ValidateAntiForgeryToken se aplica a los métodos de acción individuales. Lo más probable es que
en este escenario para un método de acción POST debe dejar no esté por equivocación, salir de la
aplicación sea vulnerable a ataques CSRF. Todas las publicaciones deben enviar el token
antifalsificación.
Las API no tienen un mecanismo automático para enviar la parte sin cookie del token.
Probablemente la implementación 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 de antifalsificación del controlador


El IgnoreAntiforgeryToken filtro se usa para eliminar la necesidad de un token antifalsificación para
una determinada acción (o controlador). Cuando se aplica, invalida este filtro
ValidateAntiForgeryToken y AutoValidateAntiforgeryToken los filtros especificados en un nivel
superior (globalmente o en un controlador).

[Authorize]
[AutoValidateAntiforgeryToken]
public class ManageController : Controller
{
[HttpPost]
[IgnoreAntiforgeryToken]
public async Task<IActionResult> DoSomethingSafe(SomeViewModel model)
{
// no antiforgery token required
}
}

Los tokens de actualización después de la autenticación


Deben actualizar los tokens después de que el usuario se autentica al redirigir al usuario a una
vista o página de Razor Pages.

Las SPA, AJAX y JavaScript


En las aplicaciones tradicionales basadas en HTML, los tokens antifalsificación se pasan al servidor
mediante los campos de formulario oculto. En las aplicaciones modernas basadas en JavaScript y
SPA, cuántas solicitudes se realizan mediante programación. Estas solicitudes AJAX pueden usar
otras técnicas (por ejemplo, los encabezados de solicitud o las 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, se podrían mitigar vulnerabilidades CSRF porque los valores del almacenamiento local
no se envían automáticamente al servidor con cada solicitud. Por lo tanto, uso de almacenamiento
local para almacenar el token antifalsificación en el cliente y enviar el token como un encabezado
de solicitud es un enfoque recomendado.
JavaScript
Con JavaScript con vistas, el token se puede crear mediante un servicio en la vista. Insertar el
Microsoft.AspNetCore.Antiforgery.IAntiforgery servicio en la vista y la llamada
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 desde el
servidor o leerlos desde el cliente.
El ejemplo anterior utiliza JavaScript para leer el valor del campo oculto para el encabezado de
POST de AJAX.
JavaScript también 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 el script solicita para enviar el token en un encabezado denominado


X-CSRF-TOKEN , configure el servicio antifalsificación para buscar el X-CSRF-TOKEN encabezado:

services.AddAntiforgery(options => options.HeaderName = "X-CSRF-TOKEN");

El ejemplo siguiente utiliza JavaScript para realizar una solicitud 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 utiliza 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 debe
establecerse explícitamente. El nombre del encabezado es X-XSRF-TOKEN . El servidor debería
detectar este encabezado y validar su contenido.
Para el trabajo de la API de ASP.NET Core con esta convención:
Configurar la aplicación para proporcionar un token en una cookie denominada XSRF-TOKEN .
Configurar el servicio que se busca un encabezado denominado antifalsificación X-XSRF-TOKEN .

services.AddAntiforgery(options => options.HeaderName = "X-XSRF-TOKEN");

Vea o descargue el código de ejemplo (cómo descargarlo)

Extender antifalsificación
El IAntiForgeryAdditionalDataProvider tipo permite a los desarrolladores extender el
comportamiento del sistema anti-CSRF por los 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. Podría devolver una marca de tiempo, un valor nonce o
cualquier otro valor y, a continuación, llamar a un implementador ValidateAdditionalData para
validar estos datos cuando se valida el token. Nombre de usuario del cliente ya se han incrustado
en los tokens generados, por lo que es necesario incluir esta información. Si un token incluye datos
suplementarios pero no IAntiForgeryAdditionalDataProvider está configurado, no se validan los
datos suplementarios.

Recursos adicionales
CSRF en Abrir Web Application Security Project (OWASP ).
Hospedaje de ASP.NET Core en una granja de servidores web
Evitar ataques de redireccionamiento abierto en
ASP.NET Core
23/08/2018 • 6 minutes to read • Edit Online

Una aplicación web que se redirige a una dirección URL que se especifica a través de la solicitud, como los datos
de cadena de consulta o formulario potencialmente ser alterada para redirigir usuarios a una dirección URL
externa, malintencionada. Esta modificación se llama a un ataque de redireccionamiento abierto.
Cada vez que la lógica de aplicación 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 redireccionamiento abierto (también conocido como abierta
redirección).

¿Qué es un ataque de redireccionamiento abierto?


Las aplicaciones Web con frecuencia redirigen a los usuarios a una página de inicio de sesión cuando acceden a
recursos que requieren autenticación. La redirección normalmente 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, le redirigirá a la dirección URL que tenían
originalmente solicitada.
Dado que la dirección URL de destino se especifica en el elemento querystring de la solicitud, un usuario
malintencionado podría alterar la cadena de consulta. Una cadena de consulta alterado podría permitir que el sitio
redirigir al usuario a un sitio externo, malintencionado. Esta técnica se denomina un ataque de redirección (o la
redirección) abierto.
Un ataque de ejemplo
Un usuario malintencionado puede desarrollar un ataque diseñado para permitir el acceso de usuarios
malintencionados a las credenciales de un usuario o información confidencial. Para iniciar el ataque, el usuario
malintencionado convence al usuario que haga clic en un vínculo a la página de inicio de sesión de su sitio con un
returnUrl querystring agregado valor a la dirección URL. Por ejemplo, considere la posibilidad de una aplicación
en contoso.com que incluye una página de inicio de sesión en
http://contoso.com/Account/LogOn?returnUrl=/Home/About . El ataque sigue estos pasos:

1. El usuario hace clic en un vínculo malintencionado a


http://contoso.com/Account/LogOn?returnUrl=http://contoso1.com/Account/LogOn (la segunda dirección URL es
"contoso1.com", no "contoso.com").
2. El usuario inicia sesión correctamente.
3. Se redirige al usuario (por el sitio) a http://contoso1.com/Account/LogOn (un sitio malintencionado que es
exactamente igual que un sitio real).
4. El usuario inicia sesión de nuevo (lo que ofrece malintencionado sus credenciales de sitio) y se redirige al sitio
real.
El usuario es probable que se considera que no se pudo su primer intento de iniciar sesión y que su segundo
intento se realiza correctamente. Más probable es que el usuario sigue siendo consciente de que están en peligro
sus credenciales.
Además de las páginas de inicio de sesión, algunos sitios proporcionan páginas de redireccionamiento o los
puntos de conexión. Imagine que su 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 dirige a
[yoursite]/Home/Redirect?url=http://phishingsite.com/Home/Login . Un usuario típico consultará la dirección URL y
que empieza con el nombre del sitio. Confiar en, hará clic en el vínculo. El redireccionamiento abierto, a
continuación, enviaría el usuario para el sitio de suplantación de identidad, que es idéntico a la suya, y
probablemente lo haría el usuario inicie sesión en lo creemos que es su sitio.

Protección contra ataques de redireccionamiento abierto


Al desarrollar aplicaciones web, tratar todos los datos proporcionados por el usuario como no es de confianza. Si
la aplicación tiene la funcionalidad que se 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 en
cualquier dirección URL que se puede proporcionar 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 URL no locales. En caso contrario, se comporta igual
que el Redirectmétodo.
IsLocalUrl
Use la IsLocalUrl método para probar las direcciones URL antes de redirigir:
El ejemplo siguiente 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 que se redirija a un sitio malintencionado. Puede
registrar los detalles de la dirección URL que se proporcionó cuando se proporciona una dirección URL de no
local en una situación donde se espera una dirección URL local. Registro redirigir direcciones URL pueden ayudar
a diagnosticar los ataques de redireccionamiento.
Evitar Cross-Site Scripting (XSS) en ASP.NET Core
30/07/2018 • 13 minutes to read • Edit Online

Por Rick Anderson


Cross-Site Scripting (XSS ) es una vulnerabilidad de seguridad que permite que un atacante colocar los scripts de
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 a través de la manipulación del DOM o redirigir el explorador a otra página. Las
vulnerabilidades XSS suelen producen cuando una aplicación toma la entrada del usuario y lo envía en una
página sin validación, codificación o secuencias de escape.

Proteger la aplicación frente a XSS


En un XSS de nivel básico funciona engañar a la aplicación en la inserción de un <script> etiqueta en la página
representada o insertando un On* eventos en un elemento. Los desarrolladores deben usar los siguientes pasos
de prevención para evitar introducir 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 de confianza están cualquier dato que puede estar controlada por un atacante, entradas
de formulario HTML, las cadenas de consulta, encabezados HTTP, incluso 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. Asegúrese de que está codificado en HTML antes de poner los datos de confianza dentro de un elemento
HTML. Codificación HTML toma como caracteres < y se modifican en un formulario seguro como &lt;
3. Antes de poner los datos que no se confía en un atributo HTML Asegúrese de que es el atributo HTML
codificado. Codificación del atributo HTML es un superconjunto de codificación HTML y codifica los
caracteres adicionales, como "y".
4. Antes de poner los datos que no se confía en JavaScript, coloque los datos en un elemento HTML cuyo
contenido se recupera en tiempo de ejecución. Si esto no es posible, asegúrese de los datos se codifica
JavaScript. Codificación de JavaScript toma caracteres peligrosos para JavaScript y se reemplaza con su
hexadecimal, por ejemplo < podría codificarse 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
está codificado como URL.

Codificación HTML con Razor


El motor de Razor uso automáticamente en MVC codifica todos salida proceden de las variables, a menos que se
trabaja muy duro para evitar que hacerlo. Usa el atributo HTML las reglas de codificación, siempre que se use la
@ directiva. Como HTML la codificación del atributo es un superconjunto de codificación HTML, que esto
significa que no tiene que preocuparse de si debe utilizar la codificación HTML o la codificación del atributo
HTML. Debe asegurarse de que utiliza solo en un contexto HTML, no cuando se intenta insertar una entrada que
no se confía directamente en JavaScript. Aplicaciones auxiliares de etiquetas también codificarán la entrada que
se usa en los parámetros tag.
Realizar la siguiente vista de 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 >. Examinando el origen, muestra la salida representada codificada como:

&lt;&quot;123&quot;&gt;

WARNING
ASP.NET Core MVC proporciona un HtmlString clase que no se codifica de forma automática tras la salida. Esto nunca
debe utilizarse en combinación con entradas no seguras como esto expondrá una vulnerabilidad XSS.

Codificación de JavaScript usa 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 datos de una etiqueta y
recuperarlo en el código 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 se 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 lo siguiente:

<script>
document.write("\u003C\u0022123\u0022\u003E");
</script>

WARNING
No concatene la entrada que no se confía en JavaScript para crear los elementos DOM. Debe usar createElement() y
asignar valores de propiedad de forma adecuada, como node.TextContent= , o use element.SetAttribute() /
element[attribute]= en caso contrario, se expone a basado en DOM 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, insértelos a través de
inserción de dependencias o puede usar los codificadores predeterminados incluidos en el
System.Text.Encodings.Web espacio de nombres. Si usa los codificadores de forma predeterminada, a
continuación, aplicado a los intervalos de caracteres se traten como seguros no surtirán efecto: los codificadores
predeterminada use las reglas de codificación más seguras posible.
Para usar los codificadores configurables a través de DI sus constructores tardará un 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;
}
}

Codificación de parámetros de URL


Si desea compilar una cadena de consulta de dirección URL con entradas no seguras 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, las comillas, signos de puntuación y otros caracteres
no seguros será porcentaje codificado en su valor hexadecimal, por ejemplo un carácter de espacio se convertirá
en % 20.

WARNING
No use la confianza de entrada como parte de una ruta de acceso de dirección URL. Pasar siempre la confianza de entrada
como un valor de cadena de consulta.

Personalización de los codificadores


De forma predeterminada, los codificadores utiliza una lista segura limitada al intervalo 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 de TagHelper Razor y HtmlHelper como utilizarán los
codificadores para las cadenas de salida.
El razonamiento sirve como protección ante errores desconocidos o futuros explorador (errores de explorador
anterior han confundiré análisis basado en el procesamiento de caracteres no válidos). Si su 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 adecuados a la aplicación
durante el inicio, en ConfigureServices() .
Por ejemplo, mediante la configuración predeterminada podría usar un HtmlHelper Razor de este modo;
<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 trata como seguro 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 el CjkUnifiedIdeographs intervalo Unicode. Ahora se
convertiría en la salida representada

<p>This link text is in Chinese: <a href="/">汉语/漢語</a></p>

Los intervalos de la 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 solo afecta a los codificadores de código abiertos 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 solo safelist se usará.

¿Dónde debe colocar tomar codificación?


General acepta práctica es que lleva a cabo en el punto de salida de codificación y los 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 permite buscar fácilmente los datos sin tener que
codificar valores antes de buscar y le permite aprovechar las ventajas de los cambios o correcciones de errores
realizadas en los codificadores.

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 quiere aceptar HTML en
la entrada de usuario - análisis de la entrada HTML es difícil, si no imposible. MarkDown y otros formatos de
texto sería una opción más segura para la entrada enriquecido. Nunca debe depender únicamente de validación.
Codificar entrada confianza antes de la salida de siempre, independientemente de qué validación ha realizado.
Habilitar solicitudes entre orígenes (CORS) en
ASP.NET Core
07/09/2018 • 19 minutes to read • Edit Online

Por Mike Wasson, Shayne Boyer, y Tom Dykstra


Seguridad del explorador impide que una página web que realizan solicitudes a un dominio diferente a la que
proviene la página web. Esta restricción se denomina el directiva del mismo origen. La directiva de mismo
origen impide que un sitio malintencionado lea datos confidenciales de otro sitio. En ocasiones, es posible que
desea permitir que otros sitios realizan solicitudes entre orígenes en la aplicación.
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 más flexible que las técnicas anteriores,
como JSONP. En este tema se muestra cómo se habilita la CORS en una aplicación ASP.NET Core.

Mismo origen
Dos direcciones URL tienen el mismo origen si tienen esquemas idénticos, los hosts y los puertos ( 6454 RFC ).
Estas dos direcciones URL tienen el mismo origen:
https://example.com/foo.html
https://example.com/bar.html

Estas direcciones URL tienen orígenes diferentes que las dos direcciones URL anteriores:
https://example.net – Dominio diferente
https://www.example.com/foo.html – Subdominio distinto
http://example.com/foo.html – Esquema diferente
https://example.com:9000/foo.html – Otro puerto

NOTE
Internet Explorer no tiene en cuenta el puerto al comparar orígenes.

Registrar servicios CORS


Referencia de la Microsoft.AspNetCore.App metapaquete o agregar una referencia de paquete para el
Microsoft.AspNetCore.Cors paquete.
Referencia de la metapaquete Microsoft.AspNetCore.All o agregar una referencia de paquete para el
Microsoft.AspNetCore.Cors paquete.
Agregue una referencia de paquete a la Microsoft.AspNetCore.Cors paquete.
Llame a AddCors en Startup.ConfigureServices para agregar servicios de la CORS al contenedor de servicios
de la aplicación:
public void ConfigureServices(IServiceCollection services)
{
services.AddCors();
}

Habilitar la CORS
Después de registrar servicios CORS, use cualquiera de los métodos siguientes para habilitar la CORS en una
aplicación ASP.NET Core:
Middleware de CORS – directivas CORS se aplican globalmente a la aplicación a través de middleware.
La CORS en MVC – las directivas de aplicar la CORS por cada acción o por cada controlador. No se usa el
Middleware de CORS.
Habilitar la CORS con Middleware de CORS
Middleware de CORS controla las solicitudes de origen cruzado a la aplicación. Para habilitar el Middleware de
CORS en la canalización de procesamiento de la solicitud, llame a la UseCors método de extensión en
Startup.Configure .

Middleware de CORS debe preceder a cualquier punto de conexión definido en la aplicación que desea admitir
las solicitudes entre orígenes (por ejemplo, antes de llamar a UseMvc de Middleware de páginas de Razor y
MVC ).
Un directiva de origen cruzado puede especificarse cuando se agrega el Middleware de CORS mediante el
CorsPolicyBuilder clase. Existen dos enfoques para definir una directiva CORS:
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!");
});

La expresión lambda toma un objeto CorsPolicyBuilder. Las opciones de configuración, tales como
WithOrigins , se describen más adelante en este tema. En el ejemplo anterior, la directiva permite que las
solicitudes entre orígenes de https://example.com y no hay otros orígenes.
Debe especificarse la dirección URL sin una barra diagonal final ( / ). Si la dirección URL termina con
/ , la comparación devuelve false y no se devuelve ningún encabezado.

CorsPolicyBuilder tiene una API fluida, por lo que se pueden encadenar llamadas de métodos:
app.UseCors(builder =>
builder.WithOrigins("http://example.com")
.AllowAnyHeader()
);

Definir una o varias directivas CORS con nombre y seleccione la directiva por su nombre en tiempo de
ejecución. En el ejemplo siguiente se agrega una directiva CORS definido por el usuario denominada
AllowSpecificOrigin. Para seleccionar la directiva, pase el nombre a UseCors :

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

Habilitar la CORS en MVC


Como alternativa, puede usar MVC para aplicar directivas específicas de CORS por cada acción o por cada
controlador. Al usar MVC para habilitar la CORS, se usan los servicios registrados de la CORS. No se usa el
Middleware de CORS.
Por acción
Para especificar una directiva CORS para una acción específica, agregue el [EnableCors] atributo 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 de un controlador concreto, agregue el [EnableCors] a la clase de
controlador. Especifique el nombre de la directiva.
[Route("api/[controller]")]
[EnableCors("AllowSpecificOrigin")]
public class ValuesController : ControllerBase

Es el orden de prioridad:
1. predeterminada
2. controlador
Deshabilitar la CORS
Para deshabilitar CORS para una acción o controlador, utilice la [DisableCors] atributo:

[HttpGet("{id}")]
[DisableCors]
public string Get(int id)
{
return "value";
}

Opciones de directiva CORS


Esta sección describen las distintas opciones que puede 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 entre orígenes
Establecer el tiempo de expiración de las comprobaciones preparatorias
Para algunas opciones, puede resultar útil leer la cómo la CORS funciona sección en primer lugar.
Establecer los orígenes permitidos
Para permitir que uno o varios orígenes específicos, llamar a WithOrigins:

options.AddPolicy("AllowSpecificOrigins",
builder =>
{
builder.WithOrigins("http://example.com", "http://www.contoso.com");
});

Para permitir que todos los orígenes, llame a AllowAnyOrigin:

options.AddPolicy("AllowAllOrigins",
builder =>
{
builder.AllowAnyOrigin();
});

Considere detenidamente antes de permitir las solicitudes desde cualquier origen. Permitir las solicitudes
desde cualquier origen significa que cualquier sitio Web puede realizar solicitudes entre orígenes en la
aplicación.
Esta configuración afecta a comprobaciones de las solicitudes y el encabezado de Access-Control-Allow -Origin
(descrita más adelante en este tema).
Establecer los métodos HTTP permitidos
Para permitir que todos los métodos HTTP, llame a AllowAnyMethod:

options.AddPolicy("AllowAllMethods",
builder =>
{
builder.WithOrigins("http://example.com")
.AllowAnyMethod();
});

Esta configuración afecta a comprobaciones de las solicitudes y el encabezado de Access-Control-Allow -


Methods (descrita más adelante en este tema).
Establecer los encabezados de solicitudes permitidos
Para permitir que los encabezados específicos que se enviarán en una solicitud de CORS, llamado crear
encabezados de solicitud, llame a WithHeaders y especifique los encabezados permitidos:

options.AddPolicy("AllowHeaders",
builder =>
{
builder.WithOrigins("http://example.com")
.WithHeaders(HeaderNames.ContentType, "x-custom-header");
});

Para permitir que todos creación encabezados de solicitud, llame a AllowAnyHeader:

options.AddPolicy("AllowAllHeaders",
builder =>
{
builder.WithOrigins("http://example.com")
.AllowAnyHeader();
});

Esta configuración afecta a comprobaciones de las solicitudes y el encabezado de Access-Control-Request-


Headers (descrita más adelante en este tema).
Una coincidencia de directiva de Middleware de CORS a encabezados específicos especificado por
WithHeaders solo es posible cuando se envían los encabezados Access-Control-Request-Headers coincidir
exactamente con los encabezados que se indica en WithHeaders .
Por ejemplo, considere una aplicación configurada como sigue:

app.UseCors(policy => policy.WithHeaders(HeaderNames.CacheControl));

Middleware de CORS rechaza una solicitud preparatoria con el siguiente encabezado de solicitud porque
Content-Language ( HeaderNames.ContentLanguage) no aparece en WithHeaders :

Access-Control-Request-Headers: Cache-Control, Content-Language

Devuelve la aplicación un 200 Aceptar respuesta pero no envía de vuelta los encabezados CORS. Por lo tanto,
el explorador no intenta la solicitud entre orígenes.
Middleware de CORS siempre permite cuatro encabezados en el Access-Control-Request-Headers para
enviarse independientemente de los valores configurados en CorsPolicy.Headers. Esta lista de encabezados
incluye:
Accept
Accept-Language
Content-Language
Origin

Por ejemplo, considere una aplicación configurada como sigue:

app.UseCors(policy => policy.WithHeaders(HeaderNames.CacheControl));

Middleware de CORS responde correctamente a una solicitud preparatoria con el siguiente encabezado de
solicitud porque Content-Language siempre es la lista de permitidos:

Access-Control-Request-Headers: Cache-Control, Content-Language

Establecer los encabezados de respuesta expuestos


De forma predeterminada, el explorador no expone todos los encabezados de respuesta a la aplicación. Para
obtener más información, consulte W3C Cross-Origin Resource Sharing (la terminología): encabezado de
respuesta Simple.
Los encabezados de respuesta que están disponibles de forma predeterminada son:
Cache-Control
Content-Language
Content-Type
Expires
Last-Modified
Pragma

La especificación de CORS llama a estos encabezados encabezados de respuesta simple. Para que otros
encabezados disponibles para la aplicación, llame a WithExposedHeaders:

options.AddPolicy("ExposeResponseHeaders",
builder =>
{
builder.WithOrigins("http://example.com")
.WithExposedHeaders("x-custom-header");
});

Credenciales en solicitudes entre orígenes


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 incluyen las cookies y
los esquemas de autenticación HTTP. Para enviar las credenciales con una solicitud entre orígenes, el cliente
debe establecer XMLHttpRequest.withCredentials a true .
Uso de XMLHttpRequest directamente:

var xhr = new XMLHttpRequest();


xhr.open('get', 'https://www.example.com/api/test');
xhr.withCredentials = true;

En jQuery:
$.ajax({
type: 'get',
url: 'https://www.example.com/home',
xhrFields: {
withCredentials: true
}

Además, el servidor debe permitir las credenciales. Para permitir que las credenciales de origen cruzado, llame
a AllowCredentials:

options.AddPolicy("AllowCredentials",
builder =>
{
builder.WithOrigins("http://example.com")
.AllowCredentials();
});

La respuesta HTTP incluye un Access-Control-Allow-Credentials encabezado, que indica al explorador que el


servidor permite credenciales para una solicitud entre orígenes.
Si el explorador envía credenciales, pero la respuesta no incluye válido Access-Control-Allow-Credentials
encabezado, el explorador no expone la respuesta a la aplicación y se produce un error en la solicitud entre
orígenes.
Tenga cuidado al permitir credenciales entre orígenes. Un sitio Web en otro dominio puede enviar las
credenciales de un usuario con sesión iniciada de la aplicación en el nombre de usuario sin el conocimiento del
usuario.
La especificación de CORS también indica ese valor orígenes a "*" (todos los orígenes) no es válido si el
Access-Control-Allow-Credentials encabezado está presente.

Solicitudes preparatorias
Para algunas solicitudes CORS, el explorador envía una solicitud adicional antes de realizar la solicitud real. Se
llama a esta solicitud una solicitud preparatoria. El explorador puede omitir la solicitud preparatoria si se
cumplen las condiciones siguientes:
El método de solicitud es GET, HEAD o POST.
La aplicación, no establezca los encabezados de solicitud distinto Accept , Accept-Language ,
Content-Language , Content-Type , o Last-Event-ID .
El Content-Type encabezado, si establece, tiene uno de uno de los siguientes valores:
application/x-www-form-urlencoded
multipart/form-data
text/plain

La regla en los encabezados de solicitud establecido para la solicitud de cliente se aplica a los encabezados de
la aplicación se establece mediante una llamada a setRequestHeader en el XMLHttpRequest objeto. La
especificación de CORS llama a estos encabezados crear encabezados de solicitud. La regla no se aplica a los
encabezados que se puede establecer el explorador, tales como User-Agent , Host , o Content-Length .
Este es un ejemplo de una solicitud de preflight:
OPTIONS https://myservice.azurewebsites.net/api/test HTTP/1.1
Accept: */*
Origin: https://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 usa 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 se establece en la
solicitud real. Como se indicó anteriormente, esto no incluye los encabezados que establece el explorador,
como User-Agent .

Una solicitud preparatoria de CORS puede incluir un Access-Control-Request-Headers encabezado, que indica
al servidor los encabezados que se envían con la solicitud real.
Para permitir encabezados específicos, llamar a WithHeaders:

options.AddPolicy("AllowHeaders",
builder =>
{
builder.WithOrigins("http://example.com")
.WithHeaders(HeaderNames.ContentType, "x-custom-header");
});

Para permitir que todos creación encabezados de solicitud, llame a AllowAnyHeader:

options.AddPolicy("AllowAllHeaders",
builder =>
{
builder.WithOrigins("http://example.com")
.AllowAnyHeader();
});

Los exploradores no están totalmente coherentes en cómo establezca Access-Control-Request-Headers . Si


establece los encabezados en algo distinto "*" (o use AllowAnyHeader), debe incluir al menos Accept ,
Content-Type , y Origin , además de los encabezados personalizados que desee admitir.

Este es un ejemplo de respuesta a la solicitud preparatoria (suponiendo que el servidor permite la solicitud):

HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Length: 0
Access-Control-Allow-Origin: https://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 Access-Control-Allow-Methods encabezado que se enumera los métodos permitidos y,


opcionalmente, un Access-Control-Allow-Headers encabezado, que se enumera los encabezados permitidos. Si
la solicitud preparatoria se realiza correctamente, el explorador envía la solicitud real.
Si se deniega la solicitud preparatoria, la aplicación devuelve un 200 Aceptar respuesta pero no envía de vuelta
los encabezados CORS. Por lo tanto, el explorador no intenta la solicitud entre orígenes.
Establecer el tiempo de expiración de las comprobaciones preparatorias
El Access-Control-Max-Age encabezado especifica cuánto tiempo puede almacenar en caché la respuesta a la
solicitud preparatoria. Para establecer este encabezado, llame a SetPreflightMaxAge:

options.AddPolicy("SetPreflightExpiration",
builder =>
{
builder.WithOrigins("http://example.com")
.SetPreflightMaxAge(TimeSpan.FromSeconds(2520));
});

Cómo funciona la CORS


Esta sección describe lo que ocurre en una solicitud de CORS en el nivel de los mensajes HTTP. Es importante
entender cómo funciona la CORS para que la directiva CORS puede ser configurada correctamente y
depurando cuando se producen 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.
El siguiente es un ejemplo de una solicitud entre orígenes. El encabezado Origin proporciona el dominio del
sitio que está realizando la solicitud:

GET https://myservice.azurewebsites.net/api/test HTTP/1.1


Referer: https://myclient.azurewebsites.net/
Accept: */*
Accept-Language: en-US
Origin: https://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 la Access-Control-Allow-Origin encabezado en la respuesta. El


valor de este encabezado ya sea con la Origin encabezado 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: https://myclient.azurewebsites.net
Date: Wed, 20 May 2015 06:27:30 GMT
Content-Length: 12

Test message

Si la respuesta no incluye el Access-Control-Allow-Origin encabezado, la solicitud de origen cruzado se


produce un error. En concreto, el explorador no permite la solicitud. Incluso si el servidor devuelve una
respuesta correcta, el explorador no disponer de la respuesta a la aplicación cliente.

Recursos adicionales
Recursos entre orígenes (CORS ) de uso compartido
Compartir cookies entre aplicaciones con ASP.NET y
ASP.NET Core
31/08/2018 • 11 minutes to read • Edit Online

Por Rick Anderson y Luke Latham


Los sitios Web a menudo constan de aplicaciones web individuales trabajan juntos. Para proporcionar una
experiencia de inicio de sesión único (SSO ), 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 cookies de ASP.NET Core.
Vea o descargue el código de ejemplo (cómo descargarlo)
En el ejemplo muestra el uso compartido entre tres aplicaciones que usan la autenticación con cookies de cookie:
Aplicación ASP.NET Core 2.0 Razor Pages sin usar ASP.NET Core Identity
Aplicación MVC de ASP.NET Core 2.0 con ASP.NET Core Identity
Aplicación MVC de ASP.NET Framework 4.6.1 con ASP.NET Identity
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 usa cualquier esquema, se debe usar de
forma coherente dentro y entre las aplicaciones de la cookie compartido como la combinación predeterminada
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.
Un común clave de protección de datos se utiliza la ubicación de almacenamiento. La aplicación de ejemplo usa
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 compartida
común.
En la aplicación de .NET Framework, el middleware de cookie de autenticación 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 comunes.
DataProtectionProvider requiere la Microsoft.AspNetCore.DataProtection.Extensions paquete NuGet.
Para obtener este paquete de aplicaciones más adelante y ASP.NET Core 2.1, hacer referencia a la
Microsoft.AspNetCore.App metapaquete. Cuando el destino es .NET Framework, agregue una
referencia de paquete a Microsoft.AspNetCore.DataProtection.Extensions .

Compartir cookies de autenticación entre aplicaciones de ASP.NET


Core
Cuando se usa ASP.NET Core Identity:
En el ConfigureServices método, use el 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 compartida
común ( SharedCookieApp en el ejemplo). Para obtener más información, consulte configurar la protección de
datos.
Al hospedar aplicaciones que comparten cookies entre subdominios, especifique un dominio común en el
Cookie.Domain propiedad. Compartir cookies entre aplicaciones en contoso.com , tales como
first_subdomain.contoso.com y second_subdomain.contoso.com , especifique el Cookie.Domain como .contoso.com :

options.Cookie.Domain = ".contoso.com";

Consulte la CookieAuthWithIdentity.Core del proyecto en el código de ejemplo (descarga).


En el Configure método, use el CookieAuthenticationOptions para configurar:
El servicio de protección de datos para las cookies.
El AuthenticationScheme para que coincida con ASP.NET 4.x.

app.AddIdentity<ApplicationUser, IdentityRole>(options =>


{
options.Cookies.ApplicationCookie.AuthenticationScheme =
"ApplicationCookie";

var protectionProvider =
DataProtectionProvider.Create(
new DirectoryInfo(@"PATH_TO_KEY_RING_FOLDER"));

options.Cookies.ApplicationCookie.DataProtectionProvider =
protectionProvider;

options.Cookies.ApplicationCookie.TicketDataFormat =
new TicketDataFormat(protectionProvider.CreateProtector(
"Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware",
"Cookies",
"v2"));
});

Cuando se usa directamente las cookies:


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 compartida
común ( SharedCookieApp en el ejemplo). Para obtener más información, consulte configurar la protección de
datos.
Al hospedar aplicaciones que comparten cookies entre subdominios, especifique un dominio común en el
Cookie.Domain propiedad. Compartir cookies entre aplicaciones en contoso.com , tales como
first_subdomain.contoso.com y second_subdomain.contoso.com , especifique el Cookie.Domain como .contoso.com :

options.Cookie.Domain = ".contoso.com";

Consulte la CookieAuth.Core del proyecto en el código de ejemplo (descarga).

app.UseCookieAuthentication(new CookieAuthenticationOptions
{
DataProtectionProvider =
DataProtectionProvider.Create(
new DirectoryInfo(@"PATH_TO_KEY_RING_FOLDER"))
});

Cifrar las claves de protección de datos en reposo


Para las implementaciones de producción, configure el DataProtectionProvider para cifrar las claves en reposo
con DPAPI o un X509Certificate. Consulte clave de cifrado en reposo para obtener más información.

services.AddDataProtection()
.ProtectKeysWithCertificate("thumbprint");

app.UseCookieAuthentication(new CookieAuthenticationOptions
{
DataProtectionProvider = DataProtectionProvider.Create(
new DirectoryInfo(@"PATH_TO_KEY_RING"),
configure =>
{
configure.ProtectKeysWithCertificate("thumbprint");
})
});

Uso compartido de cookies de autenticación entre ASP.NET 4.x y


aplicaciones de ASP.NET Core
Las aplicaciones ASP.NET 4.x que usar el middleware de autenticación de cookies de Katana pueden configurarse
para generar las cookies de autenticación que son compatibles con el middleware de autenticación de cookies de
ASP.NET Core. Esto permite actualizar aplicaciones individuales de un sitio grande por etapas al tiempo que
proporciona una experiencia de inicio de sesión único fluida en todo el sitio.
Cuando una aplicación usa el middleware de autenticación de cookies de Katana, llama a UseCookieAuthentication
en el proyecto Startup.Auth.cs archivo. Proyectos de aplicación web de ASP.NET 4.x crean con Visual Studio 2013
y usan más adelante el middleware de autenticación de cookies 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álida.
Una aplicación ASP.NET 4.x debe tener como destino .NET Framework 4.5.1 o posterior. En caso contrario, no
instale los paquetes de NuGet necesarios.
Para compartir las cookies de autenticación entre una aplicación ASP.NET 4.x y una aplicación ASP.NET Core,
configurar la aplicación de ASP.NET Core, como se indicó anteriormente, a continuación, configure la aplicación
ASP.NET 4.x siguiendo estos pasos:
1. Instale 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íquela 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 cookies de ASP.NET Core. Proporcionar una instancia de un DataProtectionProvider
inicializado en 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 usado por todas las
aplicaciones que comparten 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 (descarga).


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 comunes


Confirme que el sistema de identidad para cada aplicación se señala a la misma base de datos de usuario. En caso
contrario, el sistema de identidades genera 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.

Recursos adicionales
Hospedaje de ASP.NET Core en una granja de servidores web
Lista segura IP de cliente para ASP.NET Core
08/09/2018 • 6 minutes to read • Edit Online

Por Damien Bowden y Tom Dykstra


En este artículo se muestra tres maneras de implementar un safelist IP (también conocido como una lista blanca)
en una aplicación ASP.NET Core. Puede usar:
Software intermedio para comprobar la dirección IP remota de todas las solicitudes.
Filtros de acción para comprobar la dirección IP remota de las solicitudes de los métodos de acción o
controladores concretos.
Filtros de páginas de Razor para comprobar la dirección IP remota de solicitudes de páginas de Razor.
La aplicación de ejemplo muestra ambos enfoques. En cada caso, una cadena que contiene las direcciones IP de
cliente aprobadas se almacena en una configuración de aplicación. El software intermedio o el filtro analiza la
cadena en una lista y comprueba si la dirección IP remota está en la lista. Si no es así, se devuelve un código de
estado HTTP 403 Prohibido.
Vea o descargue el código de ejemplo (cómo descargarlo)

La lista segura
La lista está configurada en el appsettings.json archivo. Es una lista delimitada por punto y coma y puede contener
direcciones IPv4 e IPv6.

{
"AdminSafeList": "127.0.0.1;192.168.1.5;::1",
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
}
}

Software intermedio
El Configure método agrega el software intermedio y pasa la cadena de safelist a él en un parámetro de
constructor.
public void Configure(
IApplicationBuilder app,
IHostingEnvironment env,
ILoggerFactory loggerFactory)
{
loggerFactory.AddNLog();

app.UseStaticFiles();

app.UseMiddleware<AdminSafeListMiddleware>(
Configuration["AdminSafeList"]);
app.UseMvc();
}

El middleware analiza la cadena en una matriz y busca la dirección IP remota de la matriz. Si no se encuentra la
dirección IP remota, el middleware devuelve HTTP 401 prohibido. Este proceso de validación se omite para las
solicitudes HTTP Get.
public class AdminSafeListMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<AdminSafeListMiddleware> _logger;
private readonly string _adminSafeList;

public AdminSafeListMiddleware(
RequestDelegate next,
ILogger<AdminSafeListMiddleware> logger,
string adminSafeList)
{
_adminSafeList = adminSafeList;
_next = next;
_logger = logger;
}

public async Task Invoke(HttpContext context)


{
if (context.Request.Method != "GET")
{
var remoteIp = context.Connection.RemoteIpAddress;
_logger.LogDebug($"Request from Remote IP address: {remoteIp}");

string[] ip = _adminSafeList.Split(';');

var bytes = remoteIp.GetAddressBytes();


var badIp = true;
foreach (var address in ip)
{
var testIp = IPAddress.Parse(address);
if(testIp.GetAddressBytes().SequenceEqual(bytes))
{
badIp = false;
break;
}
}

if(badIp)
{
_logger.LogInformation(
$"Forbidden Request from Remote IP address: {remoteIp}");
context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return;
}
}

await _next.Invoke(context);

}
}

Filtro de acción
Si desea una lista segura solo para los métodos de acción o controladores concretos, use un filtro de acción. Por
ejemplo:
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Authorization;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;

namespace ClientIpAspNetCore.Filters
{
public class ClientIdCheckFilter : ActionFilterAttribute
{
private readonly ILogger _logger;
private readonly string _safelist;

public ClientIdCheckFilter
(ILoggerFactory loggerFactory, IConfiguration configuration)
{
_logger = loggerFactory.CreateLogger("ClientIdCheckFilter");
_safelist = configuration["AdminSafeList"];
}

public override void OnActionExecuting(ActionExecutingContext context)


{
_logger.LogInformation(
$"Remote IpAddress: {context.HttpContext.Connection.RemoteIpAddress}");

var remoteIp = context.HttpContext.Connection.RemoteIpAddress;


_logger.LogDebug($"Request from Remote IP address: {remoteIp}");

string[] ip = _safelist.Split(';');

var bytes = remoteIp.GetAddressBytes();


var badIp = true;
foreach (var address in ip)
{
var testIp = IPAddress.Parse(address);
if (testIp.GetAddressBytes().SequenceEqual(bytes))
{
badIp = false;
break;
}
}

if (badIp)
{
_logger.LogInformation(
$"Forbidden Request from Remote IP address: {remoteIp}");
context.Result = new StatusCodeResult(401);
return;
}

base.OnActionExecuting(context);
}
}
}

El filtro de acción se agrega al contenedor de servicios.


public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<ClientIdCheckFilter>();

services.AddMvc(options =>
{
options.Filters.Add
(new ClientIdCheckPageFilter
(_loggerFactory, Configuration));
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}

El filtro, a continuación, puede usarse en un método de acción o controlador.

[ServiceFilter(typeof(ClientIdCheckFilter))]
[HttpGet]
public IEnumerable<string> Get()

En la aplicación de ejemplo, el filtro se aplica a la Get método. Por lo que al probar la aplicación mediante el envío
de un Get solicitud de API, el atributo está validando la dirección IP del cliente. Cuando se prueba mediante una
llamada a la API con cualquier otro método HTTP, el middleware está validando la IP del cliente.

Filtrar las páginas de Razor


Si desea una lista segura para una aplicación de páginas de Razor, use un filtro de las páginas de Razor. Por
ejemplo:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using System;
using System.Linq;
using System.Net;

namespace ClientIpAspNetCore
{
public class ClientIdCheckPageFilter : IPageFilter
{
private readonly ILogger _logger;
private readonly string _safelist;

public ClientIdCheckPageFilter
(ILoggerFactory loggerFactory, IConfiguration configuration)
{
_logger = loggerFactory.CreateLogger("ClientIdCheckPageFilter");
_safelist = configuration["AdminSafeList"];
}

public void OnPageHandlerExecuting(PageHandlerExecutingContext context)


{
_logger.LogInformation(
$"Remote IpAddress: {context.HttpContext.Connection.RemoteIpAddress}");

var remoteIp = context.HttpContext.Connection.RemoteIpAddress;


_logger.LogDebug($"Request from Remote IP address: {remoteIp}");

string[] ip = _safelist.Split(';');

var bytes = remoteIp.GetAddressBytes();


var badIp = true;
foreach (var address in ip)
{
var testIp = IPAddress.Parse(address);
if (testIp.GetAddressBytes().SequenceEqual(bytes))
{
badIp = false;
break;
}
}

if (badIp)
{
_logger.LogInformation(
$"Forbidden Request from Remote IP address: {remoteIp}");
context.Result = new StatusCodeResult(401);
return;
}
}

public void OnPageHandlerExecuted(PageHandlerExecutedContext context)


{
}

public void OnPageHandlerSelected(PageHandlerSelectedContext context)


{
}
}
}

Este filtro está habilitado, éste se agrega a la colección de filtros de MVC.


public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<ClientIdCheckFilter>();

services.AddMvc(options =>
{
options.Filters.Add
(new ClientIdCheckPageFilter
(_loggerFactory, Configuration));
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}

Cuando ejecute la aplicación y solicite una página de Razor, el filtro de las páginas de Razor está validando la IP
del cliente.

Pasos siguientes
Más información sobre el Middleware de ASP.NET Core.
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
29/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 caché en memoria en ASP.NET Core
21/09/2018 • 15 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 de almacenamiento en caché


Almacenamiento en caché puede mejorar significativamente el rendimiento y 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é hace una copia de datos que pueden devolverse
mucho más rápido que el origen original. Debe escribir y probar la aplicación para que no dependa nunca
datos almacenados en caché.
ASP.NET Core admite varias memorias caché diferentes. La memoria caché más sencilla se basa en el
IMemoryCache, que representa una memoria caché que se almacenan 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
van desde un cliente de todas las solicitudes posteriores al mismo servidor. Por ejemplo, el uso de aplicaciones
Web de Azure enrutamiento de solicitud de aplicación (ARR ) para enrutar todas las solicitudes posteriores al
mismo servidor.
Las sesiones que 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 caché distribuida puede
admitir mayor escalabilidad horizontal de una caché en memoria. Utilizando una caché distribuida, descarga la
memoria caché a un proceso externo.
El IMemoryCache caché expulsará las entradas de caché bajo presión de memoria, a menos que el 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 la memoria caché distribuida
se limita a byte[] .

System.Runtime.Caching/MemoryCache
System.Runtime.Caching/MemoryCache (Paquete NuGet) se pueden usar con:
.NET standard 2.0 o posterior.
Cualquier implementación .NET que tiene como destino .NET Standard 2.0 o posterior. Por ejemplo,
ASP.NET Core 2.0 o posterior.
.NET framework 4.5 o posterior.
Microsoft.Extensions.Caching.Memory / IMemoryCache (descrita en este tema) es preferible a
System.Runtime.Caching / MemoryCache porque se integra mejor en ASP.NET Core. Por ejemplo, IMemoryCache
funciona de forma nativa con ASP.NET Core inserción de dependencias.
Use System.Runtime.Caching / MemoryCache como un puente de compatibilidad al trasladar código de ASP.NET
4.x a ASP.NET Core.
Directrices de caché
Código debería tener siempre una opción de reserva para capturar los datos y no dependen de un valor
almacenado en caché que están disponibles.
La memoria caché usa un recurso escaso, la memoria. Limitar el crecimiento de la memoria caché:
Hacer no utilizar entrada externa como las claves de caché.
Utilice la caducidad para limitar el crecimiento de la memoria caché.
Usar SetSize, tamaño y SizeLimit para limitar el tamaño de caché

Uso de IMemoryCache
Almacenamiento en caché en memoria es un servicio que se hace referencia desde la aplicación mediante
inserción de dependencias. 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 el 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.


IMemoryCacherequiere el paquete NuGet Microsoft.Extensions.Caching.Memory, que está disponible en el
metapaquete Microsoft.AspNetCore.All.
IMemoryCache requiere el paquete NuGet Microsoft.Extensions.Caching.Memory, que está disponible en el
Microsoft.AspNetCore.App metapaquete.
El siguiente código utiliza TryGetValue para comprobar si es una hora en la memoria caché. Si no se almacena
en caché una vez, se crea y se agrega a la caché con una nueva entrada establecer.
public static class CacheKeys
{
public static string Entry { get { return "_Entry"; } }
public static string CallbackEntry { get { return "_Callback"; } }
public static string CallbackMessage { get { return "_CallbackMessage"; } }
public static string Parent { get { return "_Parent"; } }
public static string Child { get { return "_Child"; } }
public static string DependentMessage { get { return "_DependentMessage"; } }
public static string DependentCTS { get { return "_DependentCTS"; } }
public static string Ticks { get { return "_Ticks"; } }
public static string CancelMsg { get { return "_CancelMsg"; } }
public static string CancelTokenSource { get { return "_CancelTokenSource"; } }
}

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>

Almacenado en caché DateTime valor permanece en la memoria caché mientras hay solicitudes dentro del
tiempo de espera (y ningún expulsión debido a presión de memoria). La siguiente imagen muestra la hora
actual y una hora anterior recuperadas de 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);
}

Consulte IMemoryCache métodos y CacheExtensions métodos para obtener una descripción de los métodos
de la memoria caché.

MemoryCacheEntryOptions
El ejemplo siguiente:
Establece la hora de expiración absoluta. Este es el tiempo máximo que puede almacenarse en caché la
entrada e impide que el elemento se vuelva demasiado obsoleta cuando continuamente se renueva el plazo
de caducidad.
Establece un tiempo de expiración variable. Las solicitudes que tienen acceso a esta 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 distinto 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);
}

Usar SetSize, tamaño y SizeLimit para limitar el tamaño de caché


Un MemoryCache instancia opcionalmente puede especificar y aplicar un límite de tamaño. El límite de tamaño
de memoria no tiene una unidad de medida definida porque la memoria caché no tiene ningún mecanismo
para medir el tamaño de las entradas. Si se establece el límite de tamaño de la memoria caché, todas las
entradas deben especificar el tamaño. El tamaño especificado está en unidades que se elige el desarrollador.
Por ejemplo:
Si la aplicación web se principalmente almacenamiento en caché de cadenas, cada tamaño de la entrada de
caché podría ser la longitud de cadena.
La aplicación puede especificar el tamaño de todas las entradas como 1 y el límite de tamaño es el número
de entradas.
El código siguiente crea una sin unidades de tamaño fijo MemoryCache accesible por inserción de
dependencias:

// using Microsoft.Extensions.Caching.Memory;
public class MyMemoryCache
{
public MemoryCache Cache { get; set; }
public MyMemoryCache()
{
Cache = new MemoryCache(new MemoryCacheOptions
{
SizeLimit = 1024
});
}
}

SizeLimit no tiene unidades. Las entradas en caché deben especificar el tamaño de las unidades que consideren
más adecuados si se ha establecido el tamaño de la memoria caché. Todos los usuarios de una instancia de la
memoria caché deben usar el mismo sistema de la unidad. Una entrada no se almacenarán si la suma de los
tamaños de entrada de caché supera el valor especificado por SizeLimit . Si no se establece ningún límite de
tamaño de caché, se omitirá el tamaño de caché en la entrada.
El siguiente código registra MyMemoryCache con el inserción de dependencias contenedor.

public void ConfigureServices(IServiceCollection services)


{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

services.AddSingleton<MyMemoryCache>();
}

MyMemoryCache se crea como una caché de memoria independientes para los componentes que son conscientes
de esta memoria caché de tamaño limitado y saber cómo establecer el tamaño de la entrada de caché
correctamente.
El siguiente código utiliza MyMemoryCache :
public class AboutModel : PageModel
{
private MemoryCache _cache;
public static readonly string MyKey = "_MyKey";

public AboutModel(MyMemoryCache memoryCache)


{
_cache = memoryCache.Cache;
}

[TempData]
public string DateTime_Now { get; set; }

public IActionResult OnGet()


{
if (!_cache.TryGetValue(MyKey, out string cacheEntry))
{
// Key not in cache, so get data.
cacheEntry = DateTime.Now.TimeOfDay.ToString();

var cacheEntryOptions = new MemoryCacheEntryOptions()


// Set cache entry size by extension method.
.SetSize(1)
// Keep in cache for this time, reset time if accessed.
.SetSlidingExpiration(TimeSpan.FromSeconds(3));

// Set cache entry size via property.


// cacheEntryOptions.Size = 1;

// Save data in cache.


_cache.Set(MyKey, cacheEntry, cacheEntryOptions);
}

DateTime_Now = cacheEntry;

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

Se puede establecer el tamaño de la entrada de caché tamaño o SetSize método de extensión:


public IActionResult OnGet()
{
if (!_cache.TryGetValue(MyKey, out string cacheEntry))
{
// Key not in cache, so get data.
cacheEntry = DateTime.Now.TimeOfDay.ToString();

var cacheEntryOptions = new MemoryCacheEntryOptions()


// Set cache entry size by extension method.
.SetSize(1)
// Keep in cache for this time, reset time if accessed.
.SetSlidingExpiration(TimeSpan.FromSeconds(3));

// Set cache entry size via property.


// cacheEntryOptions.Size = 1;

// Save data in cache.


_cache.Set(MyKey, cacheEntry, cacheEntryOptions);
}

DateTime_Now = cacheEntry;

return RedirectToPage("./Index");
}

Dependencias de caché
El ejemplo siguiente muestra cómo expiran 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 expulsan.
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é 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 los
desencadenadores y los valores de expiración.

Notas adicionales
Cuando se usa una devolución de llamada para rellenar un elemento de caché:
Varias solicitudes pueden encontrar el valor almacenado en caché de clave vacía porque no se ha
completado la devolución de llamada.
Esto puede dar lugar a que varios subprocesos volver a rellenar el elemento en caché.
Cuando una entrada de caché se utiliza para crear otro, el elemento secundario copia los tokens de
expiración y la configuración de basado en tiempo de expiración de la entrada principal. El elemento
secundario no está expirada mediante la eliminación manual o actualización de la entrada principal.
Use PostEvictionCallbacks para establecer las devoluciones de llamada que se desencadena después de
la entrada de caché se expulsa de la memoria caché.

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
Asistente de etiquetas de caché
Asistente de etiquetas de caché distribuida
Trabajar con una memoria caché distribuida en
ASP.NET Core
19/07/2018 • 11 minutes to read • Edit Online

Por Steve Smith


Las memorias caché distribuidas pueden mejorar el rendimiento y escalabilidad de las aplicaciones ASP.NET
Core, especialmente cuando se hospeda en la nube o una granja de servidores.
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 de las solicitudes realizadas a él (que con varias cachés en
memoria o no almacenar en 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 caché, una 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 a los
elementos se agregan, recuperar y quita de la implementación de caché distribuida. El IDistributedCache
interfaz incluye los siguientes métodos:
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 de SetAsync
Agrega un elemento (como byte[] ) a la memoria caché utilizando una clave de cadena.
Actualización, RefreshAsync
Actualiza un elemento en la memoria caché en función de su clave, restablecer el tiempo de espera de
expiración deslizante (si existe).
Quitar, aplica removeasync a
Quita una entrada de caché en función de 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 agregarlo al contenedor no existe.

3. Desde la aplicación Middleware o las clases de controlador MVC, solicitar una instancia de
IDistributedCache desde el constructor. La instancia se proporcionarán con inserción de
dependencias (DI).

NOTE
No hay ninguna necesidad de usar una duración Singleton o en el ámbito para IDistributedCache instancias (al
menos para las implementaciones integradas). También puede crear una instancia siempre es posible que necesite uno
(en lugar de usar inserción de dependencias), pero esto puede hacer que el código sea más difícil de probar y se
infringe la dependencias explicitas.

El ejemplo siguiente 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)


{
var startTimeUTC = "Not found";
var cacheStartTimeUTC = await _cache.GetAsync("lastServerStartTimeUTC");

if (cacheStartTimeUTC != null)
{
startTimeUTC = Encoding.UTF8.GetString(cacheStartTimeUTC);
}

httpContext.Response.Headers.Append(
"Last-Server-Start-Time-UTC", startTimeUTC);

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é es leer, pero nunca se ha escrito.En este ejemplo, el valor
se establece solo cuando se inicia un servidor y no cambia. En un escenario de varios servidor, el servidor
más reciente para iniciar sobrescribirá los valores anteriores que se establecieron con otros servidores. El
Get y Set métodos usan 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 de Startup.cs muestra el valor que se va a establecer:


public void Configure(IApplicationBuilder app,
IDistributedCache cache)
{
var startTimeUTC = DateTime.UtcNow.ToString();
byte[] encodedStartTimeUTC = Encoding.UTF8.GetBytes(startTimeUTC);
var cacheEntryOptions = new DistributedCacheEntryOptions()
.SetSlidingExpiration(TimeSpan.FromSeconds(30));
cache.Set("lastServerStartTimeUTC", encodedStartTimeUTC, cacheEntryOptions);

Puesto que está configurado en el ConfigureServices método, está disponible para el


IDistributedCache
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é en 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 usarlo de forma local, y puede configurar un Azure Redis Cache para las
aplicaciones de ASP.NET Core hospedadas en Azure. La aplicación ASP.NET Core configura la
implementación de caché mediante un RedisDistributedCache instancia.
La caché en Redis requiere Microsoft.Extensions.Caching.Redis
Configurar la implementación de Redis en ConfigureServices y obtener acceso a él en el código de
aplicación mediante la solicitud de 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 el ConfigureStagingServices método configura el RedisCache :

public void ConfigureStagingServices(IServiceCollection services)


{
services.AddDistributedRedisCache(options =>
{
options.Configuration = "localhost";
options.InstanceName = "SampleInstance";
});
}

Para instalar Redis en el equipo local, instale el paquete de chocolatey https://chocolatey.org/packages/redis-


64/ y ejecute 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 almacén de respaldo. 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 especifique.
Agregar SqlConfig.Tools a la <ItemGroup> elemento del archivo de proyecto y ejecute 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 en SQL Server ejecutando el 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 tiene el siguiente esquema:

Al igual que todas las implementaciones de caché, debe obtener y establecer los valores de la memoria caché
utilizando una instancia de la aplicación IDistributedCache , no un SqlServerCache . El ejemplo implementa
SqlServerCache en el entorno de producción (por lo que está configurado 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 deben 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 su aplicación, opte por 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 de SQL
Server, puede confiar en 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 usados en una memoria caché y almacenar todos los datos en un almacén persistente
de back-end, como SQL Server o Azure Storage. Caché en Redis es una solución de almacenamiento en
caché que ofrece alto rendimiento y baja latencia en comparación con la memoria caché de SQL.

Recursos adicionales
Redis Cache en Azure
SQL Database en Azure
Almacenar en caché en memoria en ASP.NET Core
Detección de cambios con tokens de cambio en ASP.NET Core
Las respuestas en caché de ASP.NET Core
Respuesta de almacenamiento en caché de Middleware en ASP.NET Core
Aplicación auxiliar de etiquetas de caché en ASP.NET Core MVC
Aplicación auxiliar de etiquetas de caché distribuida en ASP.NET Core
Hospedaje de ASP.NET Core en una granja de servidores web
Las respuestas en caché de ASP.NET Core
29/06/2018 • 17 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:

[ResponseCache(VaryByHeader = "User-Agent", Duration = 30)]


public IActionResult About2()
{

[ResponseCache(VaryByHeader = "User-Agent", Duration = 30)]


public IActionResult About2()
{

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:

[ResponseCache(Location = ResponseCacheLocation.None, NoStore = true)]


public IActionResult Error()
{
return View();
}

[ResponseCache(Location = ResponseCacheLocation.None, NoStore = true)]


public IActionResult Error()
{
return View();
}

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:

[ResponseCache(Duration = 60)]
public IActionResult Contact()
{
ViewData["Message"] = "Your contact page.";

return View();
}

[ResponseCache(Duration = 60)]
public IActionResult Contact()
{
ViewData["Message"] = "Your contact page.";

return View();
}

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

public void ConfigureServices(IServiceCollection services)


{
services.AddMvc(options =>
{
options.CacheProfiles.Add("Default",
new CacheProfile()
{
Duration = 60
});
options.CacheProfiles.Add("Never",
new CacheProfile()
{
Location = ResponseCacheLocation.None,
NoStore = true
});
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(options =>
{
options.CacheProfiles.Add("Default",
new CacheProfile()
{
Duration = 60
});
options.CacheProfiles.Add("Never",
new CacheProfile()
{
Location = ResponseCacheLocation.None,
NoStore = true
});
});
}

Hacer referencia a un perfil de caché:

[ResponseCache(Duration = 30)]
public class HomeController : Controller
{
[ResponseCache(CacheProfileName = "Default")]
public IActionResult Index()
{
return View();
}

[ResponseCache(Duration = 30)]
public class HomeController : Controller
{
[ResponseCache(CacheProfileName = "Default")]
public IActionResult Index()
{
return View();
}

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
06/09/2018 • 15 minutes to read • Edit Online

Por Halter y John Luo


Ver o descargar el código de ejemplo de ASP.NET Core 2.1 (descarga)
En este artículo se explica cómo configurar el Middleware de almacenamiento en caché de respuestas en una
aplicación ASP.NET Core. El middleware determina cuando las respuestas son almacenables en caché, las
respuestas de los almacenes y respuestas de sirve de memoria caché. Para obtener una introducción al
almacenamiento en caché de HTTP y el ResponseCache atributo, vea almacenamiento en caché de respuesta.

Package
Referencia de la Microsoft.AspNetCore.App metapaquete o agregar una referencia de paquete para el
Microsoft.AspNetCore.ResponseCaching paquete.
Referencia de la metapaquete Microsoft.AspNetCore.All o agregar una referencia de paquete para el
Microsoft.AspNetCore.ResponseCaching paquete.
Agregue una referencia de paquete a la Microsoft.AspNetCore.ResponseCaching paquete.

Configuración
En Startup.ConfigureServices , agregue el middleware a la colección de servicios.

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 almacenables en caché
hasta 10 segundos. El ejemplo envía un Vary encabezado para configurar el middleware para servir 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) =>


{
// For GetTypedHeaders, add: using Microsoft.AspNetCore.Http;
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 respuestas solo almacena en caché las respuestas del servidor
que como resultado un código de estado 200 (OK). Otras respuestas, incluidos páginas de error, se omiten el
middleware.

WARNING
Las respuestas que incluye contenido de los clientes autenticados deben marcarse como no almacenable en caché para
evitar que el software intermedio de almacenar y atender las respuestas. Consulte condiciones para almacenar en caché
para obtener más información sobre cómo el middleware determina si una respuesta se puede almacenar en caché.

Opciones
El middleware ofrece tres opciones para el control del almacenamiento en caché de respuesta.

OPCIÓN DESCRIPCIÓN

UseCaseSensitivePaths Determina si se almacenan en caché las respuestas en las


rutas de acceso distingue mayúsculas de minúsculas. El valor
predeterminado es false .

MaximumBodySize El tamaño más grande almacenables 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 la memoria caché


de respuesta en bytes. El valor predeterminado es
100 * 1024 * 1024 (100 MB).

El ejemplo siguiente configura el middleware de:


Almacenar en caché las respuestas menores o iguales a 1.024 bytes.
Store las respuestas por las rutas de acceso distingue mayúsculas de minúsculas (por ejemplo, /page1 y
/Page1 se almacenan por separado).

services.AddResponseCaching(options =>
{
options.UseCaseSensitivePaths = true;
options.MaximumBodySize = 1024;
});

VaryByQueryKeys
Al usar 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 el
almacenamiento en caché de respuesta. El único parámetro de la ResponseCache atributo que requiere
estrictamente el middleware es VaryByQueryKeys , que no se corresponde con un encabezado HTTP real. Para
obtener más información, consulte del atributo ResponseCache.
Cuando no se usa el ResponseCache atributo, el almacenamiento en caché de respuesta puede modificarse 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 la memoria caché por todos los parámetros de consulta
de solicitud.

Encabezados HTTP utilizadas por el Middleware de almacenamiento


en caché de respuestas
Almacenamiento en caché de respuesta por el middleware se configura mediante los encabezados HTTP.

HEADER DETALLES

Autorización La respuesta no se almacenan en caché si existe el


encabezado.
HEADER DETALLES

Control de caché El middleware solo tiene en cuenta de almacenamiento en


caché las respuestas marcadas con el public la directiva
de caché. Controlar el almacenamiento en caché con los
parámetros siguientes:
max-age
Max-stale†
min-nuevo
Directiva must-revalidate
no almacenar en caché
no-store
solo-if-cached
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: las


directivas de solicitud de Cache-Control.

pragma Un Pragma: no-cache encabezado de 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. Se considera para mantener la compatibilidad con
HTTP/1.0.

Set-Cookie. La respuesta no se almacenan en caché si existe el


encabezado. Cualquier software intermedio en la
canalización de procesamiento de solicitudes que establece
una o más cookies impide que el Middleware de
almacenamiento en caché de respuestas de almacenamiento
en caché la respuesta (por ejemplo, el proveedor TempData
basado en cookie).

Variar El Vary encabezado se usa para variar 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 de
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 que se considera obsoleta en este


encabezado no es almacenar o recuperar 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 envía 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 envía una
respuesta 304 (no modificado).

Fecha Cuando se trabaja desde la memoria caché, el Date


encabezado será ajustado por el middleware si no se
proporciona 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 proporciona en la respuesta original.

Edad El Age se omite el encabezado enviado en la respuesta


original. El middleware calcula un nuevo valor al
proporcionar una respuesta almacenada en caché.

Almacenamiento en caché respeta las directivas de solicitud de


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 solicitudes con un no-cache fuerza el servidor para generar una nueva respuesta para cada
solicitud y el valor de encabezado. Actualmente, no hay ningún control del desarrollador sobre este
comportamiento de almacenamiento en caché cuando se usa el software intermedio porque el middleware se
adhiere a la especificación oficial de almacenamiento en caché.
Para obtener más control sobre el comportamiento de almacenamiento en caché, 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
Caché de aplicación auxiliar de etiquetas en ASP.NET Core MVC
Aplicación auxiliar de etiquetas de caché distribuida

Solución de problemas
Si el comportamiento de almacenamiento en caché no es como se esperaba, confirme que las respuestas son
almacenables en caché y es capaz de que se sirven desde la memoria caché. Examine los encabezados de la
solicitud entrante y los encabezados de la respuesta saliente. Habilitar registro para ayudar con la depuración.
Cuando las pruebas y solución de problemas de comportamiento de almacenamiento en caché, un explorador
puede establecer encabezados de solicitud que afectan al almacenamiento en caché de manera no deseada. Por
ejemplo, puede establecer un explorador el 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 almacenar en caché
La solicitud debe dar como resultado 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 respuestas.
El Authorization encabezado no debe estar presente.
Cache-Control parámetros del encabezado deben ser válidos y se debe marcar la respuesta public y no
marcado private .
El Pragma: no-cache encabezado no debe estar presente si el Cache-Control encabezado no está presente,
como la Cache-Control encabezado reemplaza el 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 * .
El Content-Length valor del encabezado (Si establecer) debe coincidir con el tamaño del cuerpo de
respuesta.
El IHttpSendFileFeature no se usa.
La respuesta no debe ser obsoleta según lo especificado por el Expires encabezado y el max-age y
s-maxage las directivas de caché.
Almacenamiento en 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 almacenables en caché según la RFC 7234 especificaciones. Por ejemplo, el no-store
directiva no debe existir en los campos de encabezado de solicitud o respuesta. Consulte sección 3:
almacenamiento de respuestas en las memorias caché de RFC 7234 para obtener más información.

NOTE
El sistema antifalsificación para generar tokens de seguridad para evitar la falsificación de solicitud entre sitios (CSRF)
attacks conjuntos el Cache-Control y Pragma encabezados a no-cache para que no se almacenan en caché las
respuestas. Para obtener información sobre cómo deshabilitar los tokens antifalsificación para los elementos de
formulario HTML, vea configuración de ASP.NET Core antifalsificación.

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
Compresión de respuesta en ASP.NET Core
25/09/2018 • 23 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 forma de reducir los tamaños de carga
es comprimir las respuestas de una aplicación.

Cuándo usar Middleware de compresión de respuestas


Use las tecnologías de compresión de respuesta basado en servidor en IIS, Apache o Nginx. El rendimiento del
middleware probablemente no coincida con los módulos de servidor. Servidor HTTP.sys y Kestrel no ofrece
actualmente compatibilidad integrada de compresión.
Use el Middleware de compresión de respuesta cuando haya:
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 mod_deflate de Apache
Nginx compresión y descompresión
Que hospeda directamente en:
Servidor HTTP.sys (anteriormente denominados WebListener)
Kestrel

Compresión de respuesta
Por lo general, cualquier respuesta comprimida de forma no nativa puede beneficiarse de la compresión de
respuesta. Respuestas de forma no nativa comprimidas normalmente incluyen: CSS, JavaScript, HTML, XML y
JSON. No debe comprimir activos comprimidos de forma nativa, como archivos PNG. Si se intenta comprimir
aún más una respuesta cifrada de forma nativa, reducción adicional ninguna pequeño en el 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 inferiores a 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 producir un archivo
comprimido mayor que el archivo descomprimido.
Cuando un cliente pueda procesar el contenido comprimido, el cliente debe informar al servidor de sus
capacidades enviando el Accept-Encoding encabezado con la solicitud. Cuando un servidor envía contenido
comprimido, debe incluir información en el Content-Encoding encabezado en cómo se codifica la respuesta
comprimida. Contenido designaciones de codificación admitidas por el middleware se muestran en la tabla
siguiente.

ACCEPT-ENCODING VALORES DE ENCABEZADO MIDDLEWARE COMPATIBLE DESCRIPCIÓN

br Sí (valor predeterminado) Formato de datos comprimidos Brotli

deflate No Formato de datos comprimidos


DEFLATE
ACCEPT-ENCODING VALORES DE ENCABEZADO MIDDLEWARE COMPATIBLE DESCRIPCIÓN

exi No Intercambio de W3C XML eficaz

gzip Sí Formato de archivo GZIP

identity Sí Identificador de "Ninguna 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


solicitados explícitamente la codificación

ACCEPT-ENCODING VALORES DE ENCABEZADO MIDDLEWARE COMPATIBLE DESCRIPCIÓN

br No Formato de datos comprimidos Brotli

deflate No Formato de datos comprimidos


DEFLATE

exi No Intercambio de W3C XML eficaz

gzip Sí (valor predeterminado) Formato de archivo GZIP

identity Sí Identificador de "Ninguna 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


solicitados explícitamente la codificación

Para obtener más información, consulte el IANA oficial de codificación lista del contenido.
El middleware le permite agregar proveedores de compresión adicional para las instalaciones personalizadas
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 ) cuando los envía al cliente para
dar prioridad a los esquemas de compresión de ponderación. Para obtener más información, consulte RFC 7231:
codificación aceptada.
Algoritmos de compresión están sujetos a un equilibrio entre la velocidad de compresión y la eficacia de la
compresión. Eficacia en este contexto se refiere al tamaño de la salida después de la compresión. El tamaño más
pequeño se logra mediante el máximo ó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 Enviado desde el cliente al servidor para indicar la codificación


esquemas aceptables para el cliente del contenido.

Content-Encoding Enviado 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.

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 tipos MIME
predeterminados que puede codificar, pero puede reemplazar
o agregar tipos MIME.

Vary Cuando envía el servidor con un valor de Accept-Encoding


a los clientes y servidores proxy, el Vary encabezado indica
al cliente o servidor proxy que debe almacenar en caché
(puede variar) 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 está comprimida y las que se almacenan en caché
las respuestas sin comprimir por separado.

Explore las características de Middleware de compresión de respuesta con el aplicación de ejemplo. En el ejemplo
muestra:
La compresión de respuestas de la aplicación con Gzip y proveedores de compresión personalizado.
Cómo agregar un tipo MIME a la lista predeterminada de los tipos MIME para compresión.

Package
Para incluir el software intermedio en un proyecto, agregue una referencia a la Microsoft.AspNetCore.App
metapaquete, que incluye el Microsoft.AspNetCore.ResponseCompression paquete.
Para incluir el software intermedio en un proyecto, agregue una referencia a la metapaquete
Microsoft.AspNetCore.All, que incluye el Microsoft.AspNetCore.ResponseCompression paquete.
Para incluir el software intermedio en un proyecto, agregue una referencia a la
Microsoft.AspNetCore.ResponseCompression paquete.

Configuración
El código siguiente muestra cómo habilitar el Middleware de compresión de respuestas para tipos MIME
predeterminados y los proveedores de compresión (Brotli y Gzip):
El código siguiente muestra cómo habilitar el Middleware de compresión de respuestas para tipos MIME
predeterminados y el proveedor de compresión Gzip:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddResponseCompression();
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)


{
app.UseResponseCompression();
}
}

NOTE
Usar 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
Proveedor de la compresión Brotli
Use la BrotliCompressionProvider para comprimir las respuestas con el formato de datos comprimidos Brotli.
Si no hay proveedores de compresión se agregan explícitamente a la CompressionProviderCollection:
El proveedor de la compresión Brotli se agrega de forma predeterminada en la matriz de proveedores de
compresión junto con el proveedor de compresión Gzip.
Valores predeterminados de compresión para la compresión Brotli cuando el formato de datos comprimidos
Brotli es compatible con el cliente. Si Brotli no es compatible con el cliente, la compresión predeterminada es
Gzip cuando el cliente admite la compresión Gzip.

public void ConfigureServices(IServiceCollection services)


{
services.AddResponseCompression();
}

Cuando se agregan explícitamente todos los proveedores de compresión, se debe agregar el proveedor de
compresión Brotoli:
public void ConfigureServices(IServiceCollection services)
{
services.AddResponseCompression(options =>
{
options.Providers.Add<BrotliCompressionProvider>();
options.Providers.Add<GzipCompressionProvider>();
options.Providers.Add<CustomCompressionProvider>();
options.MimeTypes =
ResponseCompressionDefaults.MimeTypes.Concat(
new[] { "image/svg+xml" });
});
}

Establecer nivel con la compresión BrotliCompressionProviderOptions . El proveedor de la compresión Brotli el


valor predeterminado es el nivel de compresión más rápido (CompressionLevel.Fastest), que no podría producir
la compresión más eficaz. Si se desea la compresión más eficaz, configure el middleware de compresión óptima.

NIVEL DE COMPRESIÓN DESCRIPCIÓN

CompressionLevel.Fastest Compresión debe completarse tan pronto como sea posible,


incluso si la salida resultante no se comprime de forma
óptima.

CompressionLevel.NoCompression No debe realizarse ninguna compresión.

CompressionLevel.Optimal Las respuestas se deben comprimir óptimamente, incluso si la


compresión tarda más tiempo en completarse.

public void ConfigureServices(IServiceCollection services)


{
services.AddResponseCompression();

services.Configure<BrotliCompressionProviderOptions>(options =>
{
options.Level = CompressionLevel.Fastest;
});
}

Proveedor de compresión gzip


Use la GzipCompressionProvider para comprimir las respuestas con el formato de archivo Gzip.
Si no hay proveedores de compresión se agregan explícitamente a la CompressionProviderCollection:
El proveedor de compresión Gzip se agrega de forma predeterminada en la matriz de proveedores de
compresión junto con el proveedor de la compresión Brotli.
Valores predeterminados de compresión para la compresión Brotli cuando el formato de datos comprimidos
Brotli es compatible con el cliente. Si Brotli no es compatible con el cliente, la compresión predeterminada es
Gzip cuando el cliente admite la compresión Gzip.
El proveedor de compresión Gzip se agrega de forma predeterminada en la matriz de proveedores de
compresión.
Compresión predeterminada en Gzip cuando el cliente admite la compresión Gzip.
public void ConfigureServices(IServiceCollection services)
{
services.AddResponseCompression();
}

El proveedor de compresión Gzip deben agregarse cuando se agregan explícitamente todos los proveedores de
compresión:

public void ConfigureServices(IServiceCollection services)


{
services.AddResponseCompression(options =>
{
options.Providers.Add<BrotliCompressionProvider>();
options.Providers.Add<GzipCompressionProvider>();
options.Providers.Add<CustomCompressionProvider>();
options.MimeTypes =
ResponseCompressionDefaults.MimeTypes.Concat(
new[] { "image/svg+xml" });
});
}

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

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

Establecer nivel con la compresión GzipCompressionProviderOptions. El proveedor de compresión Gzip el valor


predeterminado es el nivel de compresión más rápido (CompressionLevel.Fastest), que no podría producir la
compresión más eficaz. Si se desea la compresión más eficaz, configure el middleware de compresión óptima.

NIVEL DE COMPRESIÓN DESCRIPCIÓN

CompressionLevel.Fastest Compresión debe completarse tan pronto como sea posible,


incluso si la salida resultante no se comprime de forma
óptima.

CompressionLevel.NoCompression No debe realizarse ninguna compresión.


NIVEL DE COMPRESIÓN DESCRIPCIÓN

CompressionLevel.Optimal Las respuestas se deben comprimir óptimamente, incluso si la


compresión tarda más tiempo en completarse.

public void ConfigureServices(IServiceCollection services)


{
services.AddResponseCompression();

services.Configure<GzipCompressionProviderOptions>(options =>
{
options.Level = CompressionLevel.Fastest;
});
}

Proveedores personalizados
Crear implementaciones de compresión personalizado con ICompressionProvider. El EncodingName representa
el contenido que este codificación ICompressionProvider genera. El middleware usa esta información para elegir
el proveedor basándose en la lista especificada en el Accept-Encoding encabezado de la solicitud.
Uso de la aplicación de ejemplo, el cliente envía una solicitud con el Accept-Encoding: mycustomcompression
encabezado. El middleware usa la implementación de compresión personalizado 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 compresión personalizado para que funcione.

public void ConfigureServices(IServiceCollection services)


{
services.AddResponseCompression(options =>
{
options.Providers.Add<BrotliCompressionProvider>();
options.Providers.Add<GzipCompressionProvider>();
options.Providers.Add<CustomCompressionProvider>();
options.MimeTypes =
ResponseCompressionDefaults.MimeTypes.Concat(
new[] { "image/svg+xml" });
});
}

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

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

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

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 se comprime en el ejemplo. No hay una implementación de la
compresión en el CustomCompressionProvider clase del ejemplo. Sin embargo, el ejemplo muestra que podría
implementar un algoritmo de compresión de este tipo.
tipos MIME
El middleware especifica un conjunto predeterminado de los tipos MIME para compresión:
application/javascript
application/json
application/xml
text/css
text/html
text/json
text/plain
text/xml

Reemplazar o anexar los tipos MIME con las opciones de Middleware de compresión de respuesta. Tenga en
cuenta que comodines MIME tipos, como text/* no se admiten. La aplicación de ejemplo agrega un tipo MIME
para image/svg+xml y comprime y sirve de imagen del banner de ASP.NET Core ( banner.svg).

public void ConfigureServices(IServiceCollection services)


{
services.AddResponseCompression(options =>
{
options.Providers.Add<BrotliCompressionProvider>();
options.Providers.Add<GzipCompressionProvider>();
options.Providers.Add<CustomCompressionProvider>();
options.MimeTypes =
ResponseCompressionDefaults.MimeTypes.Concat(
new[] { "image/svg+xml" });
});
}
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" });
});
}

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

Compresión con el protocolo seguro


Pueden controlar las respuestas comprimidas a través de conexiones seguras con el EnableForHttps opción, que
está deshabilitada de forma predeterminada. Uso de compresión con páginas generadas dinámicamente puede
dar lugar a problemas de seguridad, como el CRIME y infracción ataques.

Agregar el encabezado Vary


Si basa la compresión de respuestas en el Accept-Encoding encabezado, hay potencialmente varias versiones
comprimidas de la respuesta y una versión sin comprimir. Con el fin de indicar a las memorias caché de cliente y
servidor proxy que existen varias versiones y deben almacenarse los Vary encabezado se agrega con un
Accept-Encoding valor. En ASP.NET Core 2.0 o posterior, se agrega el middleware la Vary automáticamente
cuando se comprime la respuesta de encabezado.
Si basa la compresión de respuestas en el Accept-Encoding encabezado, hay potencialmente varias versiones
comprimidas de la respuesta y una versión sin comprimir. Con el fin de indicar a las memorias caché de cliente y
servidor proxy que existen varias versiones y deben almacenarse los Vary encabezado se agrega con un
Accept-Encoding valor. En ASP.NET Core 1.x, agregar el Vary encabezado a la respuesta se lleva a cabo
manualmente:

// ONLY REQUIRED FOR ASP.NET CORE 1.x APPS


private void ManageVaryHeader(HttpContext context)
{
// If the Accept-Encoding header is present, add the Vary header
var accept = context.Request.Headers[HeaderNames.AcceptEncoding];
if (!StringValues.IsNullOrEmpty(accept))
{
context.Response.Headers.Append(HeaderNames.Vary, HeaderNames.AcceptEncoding);
}
}

Problema de middleware cuando están detrás de un proxy inverso de


Nginx
Cuando una solicitud se redirigió mediante un proxy nginx, el Accept-Encoding se quita el encabezado. Esto
impide que el middleware de compresión de la respuesta. Para obtener más información, consulte NGINX:
compresión y descompresión. Este problema es controlando averiguar la 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, deshabilite el módulo con una adición a la web.config archivo. Para más
información, vea Disabling IIS modules (Deshabilitación de módulos de IIS ).

Solución de problemas
Usar una herramienta como Fiddler, Firebug, o Postman, que permiten establecer el Accept-Encoding encabezado
de solicitud y estudiar los encabezados de respuesta, el tamaño y el cuerpo. De forma predeterminada, el
Middleware de compresión de respuestas comprime las respuestas que cumplen las condiciones siguientes:
El Accept-Encoding está presente con un valor de encabezado br , gzip , * , o codificación personalizada que
coincide con un proveedor de compresión personalizado que haya establecido. El valor no debe ser identity
o tiene un valor de calidad (qvalue, q ) de 0 (cero).
El tipo MIME ( Content-Type ) deben 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 el protocolo no seguro (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.
El Accept-Encoding está presente con un valor de encabezado gzip , * , o codificación personalizada que
coincide con un proveedor de compresión personalizado que haya establecido. El valor no debe ser identity
o tiene un valor de calidad (qvalue, q ) de 0 (cero).
El tipo MIME ( Content-Type ) deben 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 el protocolo no seguro (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 la aplicación en ASP.NET Core
Middleware de ASP.NET Core
Mozilla Developer Network: Codificación aceptada
La sección RFC 7231 3.1.2.1: Códigos de contenido
RFC 7230 sección 4.2.3: Codificación Gzip
Versión de especificación del formato de archivo GZIP 4.3
Migración a ASP.NET Core
11/07/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

ASP.NET Core 2.0 a 2.1


Migrar de ASP.NET Core 2.0 a 2.1
Migración de ASP.NET Core 2.0 a 2.1
27/09/2018 • 17 minutes to read • Edit Online

Por Rick Anderson


Consulte 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:
Cubre los aspectos básicos de la migración de una aplicación ASP.NET Core 2.0 a 2.1.
Proporciona información general de los cambios 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.
Eliminar WebApp1 y crear 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.
En este artículo se 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 es posible que
requieren más pasos dependiendo de las opciones seleccionadas cuando se creó el proyecto y las modificaciones
realizadas en el proyecto.

Actualización del archivo de proyecto para que use las versiones 2.1
Actualice el archivo de proyecto:
Cambiar la plataforma de destino a .NET Core 2.1 actualizando el archivo de proyecto a
<TargetFramework>netcoreapp2.1</TargetFramework> .
Reemplace la referencia de paquete para Microsoft.AspNetCore.All con una referencia de paquete para
Microsoft.AspNetCore.App . Es posible que deba agregar las dependencias que se quitaron de
Microsoft.AspNetCore.All . Para obtener más información, consulte Metapaquete Microsoft.AspNetCore.All
para ASP.NET Core 2.0 y Metapaquete Microsoft.AspNetCore.App para ASP.NET Core 2.1 y versiones
posteriores.
Quite el atributo "Version" en la referencia de paquete a Microsoft.AspNetCore.App . Los proyectos que usan
<Project Sdk="Microsoft.NET.Sdk.Web"> no es necesario establecer la versión. La versión se implícito en la
plataforma de destino y seleccionada para que mejor coincida con el funcionamiento de ASP.NET Core 2.1.
(Vea a continuación para obtener más información).
Para las aplicaciones que tienen como destino .NET Framework, actualizar cada referencia de paquete a 2.1.
Quite las referencias a <DotNetCliToolReference> elementos para los siguientes paquetes. Estas
herramientas se agrupan de forma predeterminada en la CLI de .NET Core y no deben instalarse por separado.
Microsoft.DotNet.Watcher.Tools ( dotnet watch )
Microsoft.EntityFrameworkCore.Tools.DotNet ( dotnet ef )
Microsoft.Extensions.Caching.SqlConfig.Tools ( dotnet sql-cache )
Microsoft.Extensions.SecretManager.Tools ( dotnet user-secrets )
Opcional: puede quitar el <DotNetCliToolReference> (elemento) para
Microsoft.VisualStudio.Web.CodeGeneration.Tools . Puede reemplazar esta herramienta con una versión
instalada globalmente mediante la ejecución de dotnet tool install -g dotnet-aspnet-codegenerator .
Para 2.1, un biblioteca de clases de Razor es la solución recomendada para distribuir archivos de Razor. Si la
aplicación utiliza vistas incrustadas, o si no se basa en la compilación en tiempo de ejecución de archivos de
Razor, agregue <CopyRefAssembliesToPublishDirectory>true</CopyRefAssembliesToPublishDirectory> al archivo de
proyecto.
El marcado siguiente muestra el archivo de 2.0 proyecto generadas por las plantillas:

<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.1.4" />
<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 muestra el archivo de 2.1 proyecto generadas por las plantillas:

<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.1"
PrivateAssets="All" />
</ItemGroup>

</Project>

Reglas para los proyectos destinados a marco compartido


ASP.NET Core 2.1 incluye los siguientes marcos compartidos:
Microsoft.AspNetCore.App
Microsoft.AspNetCore.All
La versión especificada por la referencia de paquete es el mínimo requerido versión. Por ejemplo, un proyecto que
hacen referencia a la 2.1.1 versiones de estos paquetes no se ejecutarán en un equipo con sólo el 2.1.0 en tiempo
de ejecución instalado.
Problemas conocidos de los proyectos destinados a marco compartido:
.NET Core 2.1.300 SDK (incluida por primera vez en Visual Studio 15.6) establezca la versión implícita de
Microsoft.AspNetCore.App a 2.1.0 que causaba entra en conflicto con Entity Framework Core 2.1.1. La
solución recomendada consiste en actualizar el SDK de .NET Core a 2.1.301 o posterior. Para obtener más
información, consulte paquetes que comparten las dependencias con Microsoft.AspNetCore.App no
pueden hacer referencia a las versiones de revisión.
Todos los proyectos que se deben usar Microsoft.AspNetCore.All o Microsoft.AspNetCore.App debe agregar
una referencia de paquete para el paquete en el archivo de proyecto, incluso si contienen una referencia de
proyecto a otro proyecto utilizando Microsoft.AspNetCore.All o Microsoft.AspNetCore.App .
Ejemplo:
MyApptiene una referencia de paquete a Microsoft.AspNetCore.App .
MyApp.Tests tiene una referencia de proyecto a MyApp.csproj .
Agregue una referencia de paquete para Microsoft.AspNetCore.App a MyApp.Tests . Para obtener más
información, consulte las pruebas de integración es difícil de configurar y dejar de funcionar en el servicio
de marco de trabajo compartido.

Actualizar a las imágenes de Docker 2.1


En ASP.NET Core 2.1, las imágenes de Docker se migran a la repositorio de GitHub dotnet/dotnet-docker. La
siguiente tabla muestra los cambios de imagen y etiqueta de Docker:

2.0 2.1

Microsoft / aspnetcore:2.0 Microsoft / dotnet:2.1-aspnetcore-runtime

Microsoft/aspnetcore-build: 2.0 Microsoft / dotnet:2.1-sdk

Cambiar el FROM líneas en su Dockerfile para usar las etiquetas y los nuevos nombres de imagen en la columna
2.1 de la tabla anterior. Para obtener más información, consulte migrar desde los repositorios de docker de
aspnetcore a dotnet.

Cambios para aprovechar las ventajas de las expresiones basadas en


código nuevo 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 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 prueba de integración.
Cambios realizados en el inicio
El código siguiente muestra los cambios al código de plantilla 2.1 generado. Todos los cambios de código, salvo
que recién agregados 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();
// If the app uses Session or TempData based on Session:
// app.UseSession();

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 estricto 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 (RCL ).
El valor predeterminado 2.1 la interfaz de usuario de identidad actualmente no proporciona nuevas características
importantes con respecto a la versión 2.0. Sustituyendo la identidad con el paquete RCL es opcional. Entre las
ventajas de reemplazar la plantilla generan identidad código con la versión RCL incluyen:
Número de archivos se mueve fuera de un árbol de origen.
Nuevas características de identidad ni correcciones de errores se incluyen en el Microsoft.AspNetCore.App
metapaquete. Obtendrá automáticamente la identidad actualizada cuando Microsoft.AspNetCore.App se
actualiza.
Si ha realizado no trivial cambios en la plantilla generan código de identidad:
Las ventajas anteriores probablemente sí lo hagan no justificar la conversión a la versión RCL.
Puede mantener el código de la identidad de ASP.NET Core 2.0, es totalmente compatible.
Identidad 2.1 expone los puntos de conexión con el Identity área. Por ejemplo, la tabla siguiente muestra
ejemplos de puntos de conexión de identidad que cambian de 2.0 a 2.1:

2.0 DIRECCIÓN URL DE 2.1 DIRECCIÓN URL DE

/ Cuenta/inicio de sesión / Identidad o cuenta/inicio de sesión

/ Cuenta/cierre de sesión / Identidad o cuenta/cierre de sesión

/ Cuenta/administrar / Identidad o cuenta/administrar

Interfaz de usuario de identidad con la necesidad de biblioteca de identidades 2.1 para tener en cuenta las
direcciones URL de identidad de las aplicaciones que tienen con la identidad del código y reemplace 2.0 tienen
/Identity segmento antepuesto a los URI. Una manera de controlar el nuevo es configurar las redirecciones, por
ejemplo desde los puntos de conexión de la identidad /Account/Login a /Identity/Account/Login .
Identidad de la actualización a la versión 2.1
Las siguientes opciones están disponibles para actualizar la identidad a 2.1.
Use el código de 2.0 de la interfaz de usuario de identidad sin realizar ningún cambio. Mediante código 2.0 de
la interfaz de usuario de identidad es totalmente compatible. Esto es una buena opción cuando se realizaron
cambios significativos en el código de identidad generado.
Elimine el código existente de identidad 2.0 y Scaffold identidad en el proyecto. El proyecto usará la ASP.NET
Core Identity biblioteca de clases de Razor. Puede generar código y la interfaz de usuario para el código de
interfaz de usuario de identidad que se ha modificado. Aplicar los cambios de código en el código de interfaz
de usuario recién con scaffolding.
Elimine el código existente de identidad 2.0 y Scaffold identidad en el proyecto con la opción reemplazar
todos los archivos.
Reemplazar la identidad 2.0 interfaz de usuario con la biblioteca de clases de Razor de identidad 2.1
En esta sección se describe los pasos para reemplazar el código de identidad de ASP.NET Core 2.0 plantilla
generada con el ASP.NET Core Identity biblioteca de clases de Razor. Los pasos siguientes son para un proyecto
de páginas de Razor, pero el enfoque para un proyecto MVC es similar.
Compruebe el se actualiza el archivo de proyecto para utilizar las 2.1 versiones
Elimine las siguientes carpetas y todos los archivos en ellos:
Controladores
Páginas/Account /
Extensiones
Compile el proyecto.
Aplicar la técnica scaffolding identidad en el proyecto:
Seleccione los proyectos salir _Layout.cshtml archivo.
Seleccione el + icono en el lado derecho de la clase de contexto de datos. Acepte el nombre
predeterminado.
Seleccione agregar para crear una nueva clase de contexto de datos. Crear un nuevo contexto de datos
es necesario para aplicar la técnica scaffolding. Quite el nuevo contexto de datos en la sección siguiente.
Actualizar después de la identidad de scaffolding
Eliminar el proveedor de scaffolding de identidad generado IdentityDbContext clase derivada de la
áreas/identidad/Data/ carpeta.
Eliminar Areas/Identity/IdentityHostingStartup.cs
Actualización de la _LoginPartial.cshtml archivo:
Mover páginas /_LoginPartial.cshtml a páginas/Shared/_LoginPartial.cshtml
Agregar asp-area="Identity" los vínculos del formulario y el delimitador.
Actualización de la <form /> elemento a
<form asp-area="Identity" asp-page="/Account/Logout" asp-route-returnUrl="@Url.Page("/Index", new {
area = "" })" method="post" id="logoutForm" class="navbar-right">

El código siguiente muestra la actualización _LoginPartial.cshtml archivo:

@using Microsoft.AspNetCore.Identity

@inject SignInManager<ApplicationUser> SignInManager


@inject UserManager<ApplicationUser> UserManager

@if (SignInManager.IsSignedIn(User))
{
<form asp-area="Identity" asp-page="/Account/Logout" asp-route-returnUrl="@Url.Page("/Index", new {
area = "" })" method="post" id="logoutForm" class="navbar-right">
<ul class="nav navbar-nav navbar-right">
<li>
<a asp-area="Identity" asp-page="/Account/Manage/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="Identity" asp-page="/Account/Register">Register</a></li>
<li><a asp-area="Identity" asp-page="/Account/Login">Log in</a></li>
</ul>
}

Actualización ConfigureServices con el código siguiente:


public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

services.AddDefaultIdentity<ApplicationUser>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();

services.AddMvc();

// Register no-op EmailSender used by account confirmation and password reset


// during development
services.AddSingleton<IEmailSender, EmailSender>();
}

Cambios en las páginas de Razor proyectos de archivos de Razor


El archivo de diseño
Mover páginas /_Layout.cshtml a páginas/Shared/_Layout.cshtml
El Layout.cshtml archivo tiene los siguientes cambios:
se agrega. Para más información, vea GDPR support in
<partial name="_CookieConsentPartial" />
ASP.NET Core (Compatibilidad con el Reglamento general de protección de datos en ASP.NET Core).
cambios de jQuery de 2.2.0 a 3.3.1
_ValidationScriptsPartial.cshtml
Páginas /_ValidationScriptsPartial.cshtml se mueve a páginas/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
Consulte RGPD se admiten 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
Los archivos nuevos y los métodos de acción
Se agrega lo siguiente:
Views/Home/Privacy.cshtml
El Privacy se agrega el método de acción para el controlador Home.

Consulte RGPD se admiten en ASP.NET Core para obtener información sobre los archivos anteriores.
Cambios adicionales
Si hospeda la aplicación en Windows con IIS, instale la última versión conjunto de hospedaje de .NET Core.
SetCompatibilityVersion
Configuración de transporte
Migración de ASP.NET a ASP.NET Core
14/09/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 o posterior

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.1.4" />
</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 más información sobre la inserción de dependencias, vea Inserción de dependencias.

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 desde ASP.NET Web API a ASP.NET Core
09/07/2018 • 14 minutes to read • Edit Online

Por Steve Smith y Scott Addie


API de Web son servicios HTTP que lleguen a una amplia gama de clientes, incluidos los exploradores y
dispositivos móviles. ASP.NET Core MVC incluye compatibilidad para la creación de API Web que proporciona
una forma ú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 a ASP.NET Core MVC.
Vea o descargue el código de ejemplo (cómo descargarlo)

Proyecto de ASP.NET Web API de revisión


Este artículo usa el proyecto de ejemplo, ProductsApp, creado en el artículo Introducción a ASP.NET Web API 2
como punto de partida. En ese proyecto, un proyecto de ASP.NET Web API sencillo 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 uno 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 enrutamiento mediante atributos, aunque realmente no se usa en el proyecto. También
configura la tabla de enrutamiento, que es utilizada por ASP.NET Web API. En este caso, ASP.NET Web API
esperará a 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 sencilla:

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 que se inicia, podemos demostramos cómo migrar este proyecto Web
API a ASP.NET Core MVC.

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 a él, 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 Web API. Se migrará el ProductsApp contenido a este nuevo
proyecto.

Eliminar el Project_Readme.html archivo desde el nuevo proyecto. La solución ahora un aspecto similar al
siguiente:

Migración de la configuración
ASP.NET Core ya no usa 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 Core MVC, el enrutamiento
basado en atributo ahora se incluye de forma predeterminada cuando UseMvc() se denomina; y, esto es el
enfoque recomendado para configurar las rutas de la API Web (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 usar el enrutamiento mediante atributos en el proyecto a partir de ahora, se necesita
ninguna configuración adicional. Solo tiene que 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 [controller ] en la línea 8. Enrutamiento basado en atributo ahora es compatible
con ciertos tokens, como [controller ] y [action]. 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ágicas en el proyecto y se garantiza que las rutas se mantienen sincronizadas con sus correspondientes
controladores y acciones cuando se aplican las refactorizaciones de cambio de nombre automático.
Para migrar el controlador de API de productos, nos debemos copiar ProductsController al nuevo proyecto. A
continuación, basta con incluir el atributo de ruta en el controlador:

[Route("api/[controller]")]

También deberá agregar el [HttpGet] atribuir a los dos métodos, ya que ambos se deben 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 Web API sencilla es copiar a través de los
controladores y los modelos que usan. En este caso, simplemente copie Controllers/ProductsController.cs desde el
proyecto original a una nueva. A continuación, copie toda la carpeta modelos del proyecto original al nuevo.
Ajustar los espacios de nombres para que coincida con el nuevo nombre del proyecto (ProductsCore). En este
momento, puede compilar la aplicación, y encontrará un número de errores de compilación. Estos deben
generalmente se dividen en las siguientes categorías:
ApiController no existe
System.Web.Http espacio de nombres no existe.
IHttpActionResult no existe
Afortunadamente, estos son muy fáciles corregir:
Cambio ApiController a controlador (es posible que deba agregar mediante Microsoft.AspNetCore.Mvc)
Eliminar cualquiera con la instrucción que hace referencia a System.Web.Http
Cambiar a cualquier método de devolución IHttpActionResult para devolver un IActionResult
Una vez que estos cambios se han realizado y que no use las instrucciones using quitado, el migrados
ProductsController clase tiene este 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 podrá 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 el primer producto.

Corrección de compatibilidad ASP.NET 4.x Web API 2


Es una herramienta muy útil cuando los proyectos de migración de ASP.NET Web API a ASP.NET Core el
Microsoft.AspNetCore.Mvc.WebApiCompatShim biblioteca. La corrección de compatibilidad amplía ASP.NET
Core para permitir a un número de diferentes convenciones de Web API 2 que se usará. El ejemplo portado
anteriormente en este documento es bastante básico que la corrección de compatibilidad no era necesaria. Para
proyectos grandes, el uso de la corrección de compatibilidad puede ser útil para temporalmente reduciendo la
brecha de 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 de Web API a ASP.NET Core. Con el tiempo, los proyectos deben actualizarse
para utilizar patrones de ASP.NET Core en lugar de depender de la corrección de compatibilidad.
Características de compatibilidad que se incluyen en Microsoft.AspNetCore.Mvc.WebApiCompatShim incluyen:
Agrega un ApiController del tipo para que los tipos de base de controladores no deben actualizarse.
Habilita el enlace de modelo de estilo Web API. ASP.NET Core MVC modelar las funciones de enlace de forma
similar a MVC 5, de forma predeterminada. Los cambios de correcciones de compatibilidad de enlace para que
sea más similar a las convenciones de enlace de modelo de Web API 2 de modelos. Por ejemplo, los tipos
complejos se enlazan automáticamente desde el cuerpo de solicitud.
Amplía el enlace de modelos para que las acciones de controlador pueden tomar parámetros de tipo
HttpRequestMessage .
Agrega los formateadores de mensajes que permite las acciones para devolver los resultados de tipo
HttpResponseMessage .
Agrega métodos de respuesta adicionales que Web API 2 acciones que haya utilizado para atender respuestas:
Generadores de HttpResponseMessage:
CreateResponse<T>
CreateErrorResponse
Métodos de resultados de acción:
BadRequestErrorMessageResult
ExceptionResult
InternalServerErrorResult
InvalidModelStateResult
NegotiatedContentResult
ResponseMessageResult
Agrega una instancia de IContentNegotiator al contenedor de inserción de dependencias de la aplicación y
tipos relacionados con la negociación de contenido de hace Microsoft.AspNet.WebApi.Client disponibles. Esto
incluye tipos como DefaultContentNegotiator , MediaTypeFormatter , etcetera.

Para usar la corrección de compatibilidad, es preciso:


Instalar el Microsoft.AspNetCore.Mvc.WebApiCompatShim paquete NuGet.
Registrar servicios de corrección de compatibilidad con el contenedor de DI de la aplicación mediante una
llamada a services.AddMvc().AddWebApiConventions() en la aplicación Startup.ConfigureServices método.
Definir rutas específicas de Web API mediante MapWebApiRoute en el IRouteBuilder en la aplicación
IApplicationBuilder.UseMvc llamar.

Resumen
Migrar un proyecto de ASP.NET Web API simple para ASP.NET Core MVC es bastante sencillo, gracias a la
compatibilidad integrada para las API Web en ASP.NET Core MVC. Las partes fundamentales, que deberá migrar
cada proyecto de ASP.NET Web API son las rutas, controladores y 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
23/08/2018 • 5 minutes to read • Edit Online

Por Steve Smith


En el artículo anterior, hemos migrar configuración de un proyecto de ASP.NET MVC a ASP.NET Core MVC. En
este artículo, nos migrar las características de administración de registro, inicio de sesión y usuario.

Configurar la identidad y pertenencia


En ASP.NET MVC, las 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 ASP.NET Core 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 para usar los servicios de
identidad y Entity Framework:

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 ASP.NET MVC: ApplicationDbContext y ApplicationUser . Cree un nuevo modelos carpeta
en el núcleo de ASP.NET del proyecto y agregarle dos clases correspondientes a estos tipos. Encontrará el
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 clara.
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 Core Identity model and override the defaults if needed.
// For example, you can rename the ASP.NET Core Identity table names and more.
// Add your customizations after calling base.OnModelCreating(builder);
}
}
}

El proyecto Web de ASP.NET Core MVC Starter no incluye mucha personalización de los usuarios, o la
ApplicationDbContext . Al migrar una aplicación real, también deberá migrar todas las propiedades personalizadas
y los métodos de usuario de la aplicación y DbContext clases, así como otras clases de modelo usa 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 se puede realizar 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á lista para admitir la autenticación y los servicios de identidad. Basta con que tenga
estas características que se exponen a los usuarios.

Migrar la lógica de inicio de sesión y registro


Con la configurada para la aplicación de servicios de identidad y acceso a datos configurado mediante Entity
Framework y SQL Server, ya estamos listos para agregar compatibilidad con inicio de sesión y registro de la
aplicación. Recuerde que anteriormente en el proceso de migración nos comentadas una referencia a
_LoginPartial en _Layout.cshtml. Ahora es momento de volver a ese código, quite los comentarios y agregue 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 de Razor denominada _LoginPartial a la Views/Shared carpeta:
Actualización _LoginPartial.cshtml con el código siguiente (reemplace 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 presenta 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 usuario y autenticación de ASP.NET Identity a ASP.NET Core.
Migrar de ClaimsPrincipal.Current
23/08/2018 • 5 minutes to read • Edit Online

En proyectos ASP.NET 4.x, era habitual usar ClaimsPrincipal.Current recuperar actual autenticado notificaciones y
la identidad del usuario. En ASP.NET Core, ya no se establece esta propiedad. Código que dependía de la debe
actualizarse para obtener la identidad del usuario autenticado actual mediante un medio diferente.

Datos específicos del contexto en lugar de datos estáticos


Cuando se usa ASP.NET Core, los valores de ClaimsPrincipal.Current y Thread.CurrentPrincipal no están
establecidos. Estas propiedades representan el estado estático, lo que generalmente evita ASP.NET Core. En su
lugar, arquitectura de ASP.NET Core es recuperar las dependencias (como la identidad del usuario actual) de las
colecciones específicas del contexto de servicio (mediante su inserción de dependencias modelo (DI)). ¿Qué es
más, Thread.CurrentPrincipal es subproceso estático, por lo que no pueden conservar los cambios en algunos
escenarios asincrónicos (y ClaimsPrincipal.Current simplemente llama a 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, observe 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 establece Thread.CurrentPrincipal y comprueba su valor antes y después de esperar
una llamada asincrónica. Thread.CurrentPrincipal es específico de la subproceso en el que se ha configurado y el
método es apropiado reanudar la ejecución en un subproceso diferente después del await. Por lo tanto,
Thread.CurrentPrincipal está presente cuando se comprueba en primer lugar, pero es null tras la llamada a
await Task.Yield() .

Obtener la identidad del usuario actual de recopilación de la aplicación de servicio de inserción de dependencias es
más estable, demasiado, puesto que se pueden insertar fácilmente las identidades de la prueba.

Recuperar el usuario actual en una aplicación ASP.NET Core


Hay varias opciones para la recuperación del usuario autenticado actual ClaimsPrincipal en ASP.NET Core en
lugar de ClaimsPrincipal.Current :
ControllerBase.User. Los controladores MVC pueden tener acceso al usuario autenticado actual con sus
usuario propiedad.
HttpContext.User. Los componentes con acceso a la actual HttpContext (middleware, por ejemplo) puede
obtener el usuario actual ClaimsPrincipal desde HttpContext.User.
Pasa del autor de llamada. Bibliotecas sin acceso a la actual HttpContext a menudo se llaman desde los
controladores o componentes de middleware y puede tener la identidad del usuario actual se pasa como un
argumento.
IHttpContextAccessor. El proyecto 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 usarse como solución alternativa. IHttpContextAccessor puede tener acceso a
la actual HttpContext (si existe). Sería una solución a corto plazo para obtener la identidad del usuario
actual en el código que aún no se ha actualizado para trabajar con la arquitectura controlada por DI de
ASP.NET Core:
Asegúrese de IHttpContextAccessor disponibles en el contenedor de inserción de dependencias
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 está disponible para código que anteriormente estaba recuperando el usuario actual de una
propiedad estática.
Recuperar el usuario actual ClaimsPrincipal mediante HttpContextAccessor.HttpContext?.User . Si este
código se usa fuera del contexto de una solicitud HTTP, el HttpContext es null.
La última opción, mediante IHttpContextAccessor , al contrario de los principios de ASP.NET Core (prefiere sobre
dependencias insertadas dependencia estática). Previsto eliminar eventualmente la dependencia de estático
IHttpContextAccessor auxiliar. Puede ser un puente útil, sin embargo, al migrar grandes aplicaciones ASP.NET
existentes que estaban usando anteriormente ClaimsPrincipal.Current .
Migrar de autenticación de pertenencia de ASP.NET a
ASP.NET Core 2.0 Identity
22/08/2018 • 11 minutes to read • Edit Online

Por Isaac Levin


En este artículo se muestra la migración del 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 las aplicaciones basadas en
la pertenencia a ASP.NET para el esquema de base de datos utilizado para ASP.NET Core Identity. Para obtener más
información sobre la migración de la autenticación basada en pertenencia de ASP.NET a ASP.NET Identity, vea migrar una
aplicación existente de la pertenencia de SQL a ASP.NET Identity. Para obtener más información acerca de ASP.NET Core
Identity, 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 de código
reutilizable para controlar la seguridad dentro de 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, 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 deben migrarse a las
tablas utilizadas por el nuevo esquema de identidad.

Esquema ASP.NET Core Identity 2.0


ASP.NET Core 2.0 sigue la identidad principio introducida en ASP.NET 4.5. Aunque se comparte el principio, la
implementación entre los marcos de trabajo es diferente, incluso entre las versiones de ASP.NET Core (consulte
migrar autenticación e identidad a ASP.NET Core 2.0).
Es la forma más rápida para ver el esquema para la identidad de ASP.NET Core 2.0 crear una nueva aplicación de
ASP.NET Core 2.0. Siga estos pasos en Visual Studio 2017:
Seleccione Archivo > Nuevo > Proyecto.
Cree un nuevo aplicación Web ASP.NET Core y denomine al proyecto CoreIdentitySample.
Seleccione ASP.NET Core 2.0 en la lista desplegable y, a continuación, seleccione aplicación Web. Esta
plantilla genera un las páginas de Razor app. 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 ASP.NET Core 2.0 usa Entity Framework Core para interactuar con la base de datos almacena los
datos de autenticación. Para la aplicación recién creada para que funcione, debe ser una base de datos para
almacenar los datos. Después de crear una nueva aplicación, la forma más rápida para inspeccionar el esquema en
un entorno de base de datos es crear la base de datos mediante migraciones de Entity Framework. Este proceso
crea una base de datos, ya sea localmente o en otra parte, 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 ASP.NET Core Identity, ejecute el Update-Database en Visual
Studio Package Manager Console ventana (PMC )—se encuentra en herramientas > Administrador de
paquetes de NuGet > Package Manager Console. Ejecución de comandos de Entity Framework admite la
PMC.
Comandos de Entity Framework usan la cadena de conexión para la base de datos especificada en appsettings.json.
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 significativamente para autenticación y 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 para 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 campos son similares a las relaciones uno a uno de la pertenencia a ASP.NET Core Identity. La
tabla anterior toma el esquema de usuario de pertenencia predeterminado y lo asigna al esquema de ASP.NET Core Identity.
Los campos personalizados que se usaron para la suscripción deben asignarse manualmente. En esta asignación, no hay
ningún mapa de las contraseñas, como los criterios de la 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 de ASP.NET existente y los datos. La otra base de datos se creó
mediante los pasos descritos anteriormente. Los comentarios están incluidas alineadas 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 initialized 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 creó anteriormente se rellena con usuarios de
pertenencia. Los usuarios necesitan cambiar sus contraseñas antes de iniciar sesión.

NOTE
Si el sistema de pertenencia tenía usuarios con los nombres de usuario que no coincide con su dirección de correo
electrónico, los cambios son necesarios para la aplicación que creó anteriormente para dar cabida a esto. La plantilla
predeterminada espera UserName y Email sea el mismo. Para situaciones en las que son diferentes, debe modificarse para
usar el proceso de inicio de sesión UserName en lugar de Email .

En el de la página de inicio de sesión, ubicado en Pages\Account\Login.cshtml.cs, quite el


PageModel
[EmailAddress] de atributo de la correo electrónico propiedad. Cambie su nombre a UserName. Esto requiere un
cambio siempre que sea EmailAddress se ha mencionado, en la vista y PageModel. El resultado es similar al
siguiente:

Pasos siguientes
En este tutorial, ha aprendido cómo migrar los usuarios de pertenencia SQL a ASP.NET Core 2.0 Identity. Para
obtener más información sobre ASP.NET Core Identity, vea Introducción a la identidad.
Migración de módulos y controladores HTTP a
middleware de ASP.NET Core
23/08/2018 • 25 minutes to read • Edit Online

Por Matt Perdeck


En este artículo se muestra cómo migrar de ASP.NET existentes módulos y controladores de system.webserver a
ASP.NET Core middleware.

Módulos y controladores para


Antes de continuar con el middleware de ASP.NET Core, en primer lugar repasemos 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 dado 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 que los módulos de procesan las solicitudes entrantes viene determinado por:
1. El ciclo de vida de aplicación, que es una serie desencadenan ASP.NET: BeginRequest,
AuthenticateRequest, etcetera. Cada módulo puede crear un controlador de eventos de uno o más.
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 su Global.asax.cs
archivo. Estos controladores se ejecutan después de los controladores de los módulos configurados.

Desde los controladores y módulos al middleware


Middleware son más sencillas que los controladores y módulos HTTP:
Los módulos, controladores, Global.asax.cs, Web.config (excepto para la configuración de IIS ) y el ciclo de
vida de aplicación han desaparecido
Se han realizado los roles de los módulos y controladores de middleware de la tecla TAB
Middleware se configuran mediante código en lugar de en Web.config
Canalización bifurcación le permite enviar solicitudes al middleware específica, 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:
Invoca en principio para cada solicitud
Puede interrumpir una solicitud, por no pasar la solicitud al siguiente middleware
Puede crear su propia respuesta HTTP
Middleware y los módulos se procesan en un orden diferente:
Orden de middleware se basa en el orden en el que se ha insertado 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 para las solicitudes, mientras que el orden de
los módulos es el mismo para las solicitudes y respuestas
Consulte 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 de módulo a 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 toma método un HttpContext y devolver un Task . El middleware 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 ha tomado de la sección escribir middleware.


El MyMiddlewareExtensions clase auxiliar resulta más fácil de configurar su middleware en su Startup clase. El
UseMyMiddleware método agrega la clase de middleware a la canalización de solicitudes. Los servicios requeridos
por el middleware se insertarán en el constructor del middleware.
El módulo puede finalizar 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 software intermedio encarga de ello llamando no Invoke en el siguiente middleware en la canalización.


Tenga en cuenta que esto no termina por completo la solicitud, porque el middleware anterior aún se invocará
cuando la respuesta ésta avanza 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.
}

Al migrar a su middleware nueva funcionalidad de su módulo, es posible que no se compila el código porque la
HttpContext clase ha cambiado significativamente en ASP.NET Core. Más adelante, verá cómo migrar a
ASP.NET Core HttpContext nuevo.

Migrar la inserción de módulo en la canalización de solicitudes


Los módulos HTTP normalmente se agregan a la canalización de solicitudes mediante 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 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 en la que insertar su 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 ya se ha indicado, no hay ningún ciclo de vida de la aplicación en ASP.NET Core y el orden en que se
procesan las respuestas de middleware es diferente del usado por módulos. Esto podría hacer que su decisión de
ordenación más difícil.
Si la ordenación se convierte en un problema, podría dividir el módulo en varios componentes de middleware
que se pueden ordenar de forma independiente.

Migrar código de controlador a 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, lo 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 al middleware correspondientes a los módulos. La única diferencia es que aquí
no hay ninguna llamada a _next.Invoke(context) . Esto tiene sentido, porque el controlador no es al final de la
canalización de solicitudes, por lo que no habrá ningún middleware siguiente para invocar.

Migrar la inserción de controlador en la canalización de solicitudes


Configurar un controlador HTTP se realiza en 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>

Podría convertir esto agregando su middleware controlador nuevo a la canalización de solicitudes en su Startup
(clase), similar al middleware convertido a partir de los módulos. El problema con este enfoque es que todas las
solicitudes tendría que enviar a su nuevo software de controlador intermedio. Sin embargo, solo desea que las
solicitudes con una extensión específica para llegar a su middleware. Esto le proporcionaría la misma
funcionalidad que tenía con el controlador HTTP.
Una solución consiste en bifurcar la canalización de solicitudes con una extensión específica, mediante el
MapWhen método de extensión. Para ello, en el mismo Configure método donde agregar 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 se pueden bifurcar 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 rama delante de su software de
controlador intermedio.
Middleware agregado a la canalización antes de la rama se invocará en todas las solicitudes; la rama tendrá
ningún impacto en ellos.

Opciones de middleware con el patrón de opciones de carga


Algunos módulos y controladores tienen opciones de configuración que se almacenan en Web.config. Sin
embargo, en ASP.NET Core se usa un nuevo modelo de configuración en lugar de Web.config.
El nuevo sistema de configuración ofrece las siguientes opciones para resolver este problema:
Insertar directamente las opciones al middleware, como se muestra en el siguiente sección.
Use la patrón de 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. Store 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, por lo que te guiaremos ese enfoque:

{
"MyMiddlewareOptionsSection": {
"Param1": "Param1Value",
"Param2": "Param2Value"
}
}

MyMiddlewareOptionsSection aquí es un nombre de sección. No debe ser el mismo que el nombre de la


clase de opciones.
3. Asociar los valores de opción de la clase de opciones
El patrón de opciones usa el marco de inserción de dependencias 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, agréguelo al generador de configuración en el 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. Asociar 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 la inserción en un controlador de


opciones.

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. Cualquier otro objeto que requiere su middleware puede insertarse
de esta manera.
Opciones de middleware a través de inyección directa de carga
El patrón de opciones tiene la ventaja que crea un acoplamiento flexible entre los valores de opciones y sus
consumidores. Una vez que haya asociado una clase de opciones con los valores de opciones real, cualquier otra
clase puede obtener acceso a las opciones mediante el marco de inserción de dependencia. No hay ninguna
necesidad de pasar los 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 usado en distintas ramas, lo que permite diferentes roles. No se puede
asociar dos objetos diferentes opciones con la clase de una de las opciones.
La solución consiste en obtener los objetos de opciones con los valores de opciones real en su Startup de clases
y las pasará directamente a cada instancia del middleware.
1. Agregar una segunda clave para appsettings.json
Para agregar un segundo conjunto de opciones para la appsettings.json , debe usar una clave nueva para
identificarlo:

{
"MyMiddlewareOptionsSection2": {
"Param1": "Param1Value2",
"Param2": "Param2Value2"
},
"MyMiddlewareOptionsSection": {
"Param1": "Param1Value",
"Param2": "Param2Value"
}
}

2. Recuperar valores de opciones y pasarlos al middleware. El Use... método de extensión (que agrega el
middleware a la canalización) es un lugar 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. Habilitar el middleware para que tome 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 al UseMiddleware ). Cuando
UseMiddleware se llama con parámetros, pasa los parámetros a su 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 nuevo HttpContext


Ya ha visto que la Invoke método en su middleware toma un parámetro de tipo HttpContext :

public async Task Invoke(HttpContext context)

HttpContext ha cambiado significativamente en ASP.NET Core. En esta sección se 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 solo si el tipo de contenido secundaria 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 muestra arriba solo una vez por solicitud. Middleware al 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 mostró anteriormente, ya se realiza desde 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 traduce en:

httpContext.Response.ContentType = "text/html";

HttpContext.Response.Output se convierte en:

string responseContent = GetResponseContent();


await httpContext.Response.WriteAsync(responseContent);

HttpContext.Response.TransmitFile
Servir como un archivo se analiza aquí.
HttpContext.Response.Headers
Enviar encabezados de respuesta resulta complicada por el hecho de que si se establece después de que nada se
ha escrito en el cuerpo de respuesta, no se enviará.
La solución es establecer un método de devolución de llamada que se llamará derecha antes de escribir en el que
se inicie la respuesta. Esto se hace mejor al principio de la Invoke método en el middleware. Resulta que 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 denominado SetHeaders :

public async Task Invoke(HttpContext httpContext)


{
// ...
httpContext.Response.OnStarting(SetHeaders, state: httpContext);

El SetHeaders el método de devolución de llamada tendría el aspecto 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 al explorador en un Set-Cookie encabezado de respuesta. Como resultado, al envío de
cookies, requiere la misma devolución de llamada que se usa 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 el método de devolución de llamada podría ser 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
Introducción a los módulos y controladores HTTP
Configuración
[Inicio de aplicaciones](xref:fundamentals/startup)
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.1.4" />
</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
23/08/2018 • 14 minutes to read • Edit Online

Por Scott Addie y Hao Kung


ASP.NET Core 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 utilizan la autenticación o Identity 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, la autenticación se configura 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, la autenticación se configura a través de servicios. Cada esquema de autenticación está
registrado en el ConfigureServices método Startup.cs. El UseIdentity método se reemplaza 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 la administración de las 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. Uso de cookies con identidad
Reemplace UseIdentity con UseAuthentication en el Configure método:

app.UseAuthentication();

Invocar el AddIdentity método en el ConfigureServices método para agregar los servicios de


autenticación de cookies.
Si lo desea, invocar el ConfigureApplicationCookie o ConfigureExternalCookie método en el
ConfigureServices método ajustar la configuración de la cookie de identidad.

services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();

services.ConfigureApplicationCookie(options => options.LoginPath = "/Account/LogIn");

2. Usar cookies sin identidad


Reemplace el UseCookieAuthentication llame al método el Configure método con
UseAuthentication :

app.UseAuthentication();

Invocar el 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 siguientes cambios en Startup.cs:
Reemplace el UseJwtBearerAuthentication llame al método el Configure método con UseAuthentication :

app.UseAuthentication();

Invocar el 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 usa la identidad, por lo que se debe establecer el esquema predeterminado
pasando JwtBearerDefaults.AuthenticationScheme a la AddAuthentication método.
Autenticación de OpenID Connect (OIDC )
Realice los siguientes cambios en Startup.cs:
Reemplace el UseOpenIdConnectAuthentication llame al método el Configure método con
UseAuthentication :

app.UseAuthentication();

Invocar el 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 siguientes cambios en Startup.cs:
Reemplace el UseFacebookAuthentication llame al método el Configure método con UseAuthentication :
app.UseAuthentication();

Invocar el 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 siguientes cambios en Startup.cs:
Reemplace el UseGoogleAuthentication llame al método el Configure método con UseAuthentication :

app.UseAuthentication();

Invocar el 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 siguientes cambios en Startup.cs:
Reemplace el UseMicrosoftAccountAuthentication llame al método el Configure método con
UseAuthentication :

app.UseAuthentication();

Invocar el 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 con Twitter


Realice los siguientes cambios en Startup.cs:
Reemplace el UseTwitterAuthentication llame al método el Configure método con UseAuthentication :

app.UseAuthentication();

Invocar el 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 la versión 1.x, el AutomaticAuthenticate y AutomaticChallenge propiedades de la AuthenticationOptions clase
base se pensada para establecerse en un esquema de autenticación único. No había una buena manera para exigir
esto.
Se han quitado en 2.0, estas dos propiedades como propiedades en cada 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 . También 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 de 2.0 si se cumple una de las condiciones siguientes:


Desea que el usuario para iniciar sesión automáticamente
Usa el [Authorize] las directivas de autorización o el atributo sin especificar esquemas
Una excepción a esta regla es la AddIdentity método. Este método agrega las cookies para usted y establece el
valor predeterminado autenticarse y Desafíe a esquemas para la cookie de aplicación
IdentityConstants.ApplicationScheme . Además, Establece el esquema predeterminado de inicio de sesión a la
cookie externa IdentityConstants.ExternalScheme .

Usar las extensiones de autenticación de HttpContext


El 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 el
Microsoft.AspNetCore.Authentication espacio de nombres.

Por ejemplo, los proyectos de 1.x 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, importe el Microsoft.AspNetCore.Authentication espacio de nombres y elimine 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 tanto anónimos y los usuarios autenticados
La primera variación que se ha descrito anteriormente se ve afectada por los cambios de 2.0.
La segunda variación que se ha descrito anteriormente se ve afectada por los cambios de 2.0. Por ejemplo, puede
permitir a los usuarios anónimos a la aplicación en IIS o HTTP.sys pero autorizando a los usuarios en el nivel de
controlador de capas. En este escenario, establezca el esquema predeterminado en
IISDefaults.AuthenticationScheme en el ConfigureServices método Startup.cs:

services.AddAuthentication(IISDefaults.AuthenticationScheme);

En consecuencia, no se pudo establecer el esquema predeterminado impide que la solicitud authorize al desafío
de trabajo.

Instancias de IdentityCookieOptions
Un efecto secundario de los 2.0 cambios es el cambio a denominado opciones en lugar de las instancias de las
opciones de cookie. Se quita la capacidad para personalizar los nombres de esquema de cookie de identidad.
Por ejemplo, use los proyectos de 1.x inserción del constructor para pasar un IdentityCookieOptions parámetro
en AccountController.cs. El esquema de autenticación externo cookie se obtiene 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 mencionados anteriormente se convierte en innecesaria en los 2.0 proyectos y el


_externalCookieScheme se puede eliminar el campo:
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 la base de navegación de Entity Framework (EF ) Core IdentityUser POCO (objeto CRL
estándar) se han quitado. Si el proyecto de 1.x usa 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 duplicadas claves externas al ejecutar migraciones de EF Core, agregue lo siguiente a su
IdentityDbContext clase OnModelCreating método (después de que el base.OnModelCreating(); llamar ):
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
// Customize the ASP.NET Core Identity model and override the defaults if needed.
// For example, you can rename the ASP.NET Core 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);
}

Reemplace GetExternalAuthenticationSchemes
El método sincrónico GetExternalAuthenticationSchemes quitó una versión asincrónica. los proyectos de 1.x 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 el 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


Un ManageLoginsViewModel objeto se usa en el ManageLogins acción de ManageController.cs. En de los proyectos
de 1.x, 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 Auth 2.0 problema en GitHub.

Вам также может понравиться